/*
 * Copyright (c) 2016, 2021 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//      Marcel Valovy - initial API and implementation
//      Dmitry Kornilov - BeanValidationHelper refactoring
//      Miroslav Kos - BeanValidationHelper refactoring
package org.eclipse.persistence.jaxb;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.eclipse.persistence.exceptions.BeanValidationException;
import org.eclipse.persistence.internal.helper.XMLHelper;
import org.eclipse.persistence.internal.security.PrivilegedAccessHelper;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * Detects external Bean Validation configuration.
 * <p>
 * Strategy:<br>
 * 1. Parse validation.xml, looking for a constraints-file reference.<br>
 * 2. For each reference, if file is found, parses the constraints file and puts all classes declared under
 * {@literal <bean class="clazz">} into {@code constraintsOnClasses} field
 * of {@link org.eclipse.persistence.jaxb.BeanValidationHelper} class
 * with {@link Boolean#TRUE} value.
 * <p>
 * This class contains resources-burdening instance fields (e.g. SAXParser) and as such was designed to be instantiated
 * once (make the instance BOUNDED) and have {@link #call()} method called on that instance once.
 * <p>
 * Not suitable for singleton (memory burden). The method #parse() will be invoked only once per class load of this
 * class. After that the instance and all its fields should be made collectible by GC.
 *
 * @author Marcel Valovy
 * @author Dmitry Kornilov
 * @author Miroslav Kos
 * @since 2.6
 */
public class ValidationXMLReader implements Callable<Map<Class<?>, Boolean>> {

    public static final String DEFAULT_PACKAGE_QNAME = "default-package";
    public static final String BEAN_QNAME = "bean";
    public static final String CONSTRAINT_MAPPING_QNAME = "constraint-mapping";
    public static final String CLASS_QNAME = "class";
    public static final String PACKAGE_SEPARATOR = ".";

    private static final String VALIDATION_XML = "META-INF/validation.xml";
    private static final Logger LOGGER = Logger.getLogger(ValidationXMLReader.class.getName());

    private final List<String> constraintsFiles = new ArrayList<>(2);

    private final Map<Class<?>, Boolean> constraintsOnClasses = new HashMap<>();

    // Created lazily
    private SAXParser saxParser;

    /**
     * Parses validation.xml.
     * @return returns a map with classes found in validation.xml as keys and true as a value. Never returns null.
     */
    @Override
    public Map<Class<?>, Boolean> call() throws Exception {
        parseValidationXML(VALIDATION_XML, validationHandler);

        if (!constraintsFiles.isEmpty()) {
            parseConstraintFiles();
        }
        return constraintsOnClasses;
    }

    /**
     * Checks if validation.xml exists.
     */
    public static boolean isValidationXmlPresent() {
        return getThreadContextClassLoader().getResource(VALIDATION_XML) != null;
    }

    private void parseConstraintFiles() {
        final class ConstrainedClassesDetector extends DefaultHandler {

            private boolean defaultPackageElement = false;
            private String defaultPackage = "";

            @Override
            public void startElement(String uri, String localName, String qName,
                                     Attributes attributes) throws SAXException {

                if (DEFAULT_PACKAGE_QNAME.equalsIgnoreCase(qName)) {
                    defaultPackageElement = true;
                } else if (BEAN_QNAME.equalsIgnoreCase(qName)) {
                    String className = defaultPackage + PACKAGE_SEPARATOR + attributes.getValue(CLASS_QNAME);
                    if (LOGGER.isLoggable(Level.INFO)) {
                        String msg = "Detected external constraints on class " + className;
                        LOGGER.info(msg);
                    }
                    try {
                        Class<?> clazz = ReflectionUtils.forName(className);
                        constraintsOnClasses.put(clazz, Boolean.TRUE);
                    } catch (ClassNotFoundException e) {
                        String errMsg = "Loading found class failed. Exception: " + e.getMessage();
                        LOGGER.warning(errMsg);
                    }
                }
            }

            @Override
            public void characters(char[] ch, int start, int length) throws SAXException {
                if (defaultPackageElement) {
                    defaultPackage = new String(ch, start, length);
                    defaultPackageElement = false;
                }
            }
        }

        // Parse constraints file referenced in validation.xml. Add all classes declared under <bean class="clazz"> to
        // org.eclipse.persistence.jaxb.BeanValidationHelper#constraintsOnClasses with value Boolean#TRUE.
        for (String file : constraintsFiles) {
            parseValidationXML(file, new ConstrainedClassesDetector());
        }
    }

    /**
     * Lazy getter for SAX parser.
     */
    private SAXParser getSaxParser() {
        if (saxParser == null) {
            try {
                SAXParserFactory factory = XMLHelper.createParserFactory(false);
                saxParser = factory.newSAXParser();
            } catch (ParserConfigurationException | SAXException e) {
                String msg = "ValidationXMLReader initialization failed. Exception: " + e.getMessage();
                LOGGER.severe(msg);
                throw new BeanValidationException(msg, e);
            }
        }
        return saxParser;
    }

    private void parseValidationXML(String constraintsFilePath, DefaultHandler handler) {
        try (InputStream validationXml = getThreadContextClassLoader().getResourceAsStream(constraintsFilePath)) {
            if (validationXml != null) {
                getSaxParser().parse(validationXml, handler);
            }
        } catch (SAXException | IOException ex) {
            LOGGER.log(Level.WARNING, "Parsing of validation.xml failed.", ex);
        }
    }

    private static ClassLoader getThreadContextClassLoader() {
        return PrivilegedAccessHelper.callDoPrivileged(
                () -> Thread.currentThread().getContextClassLoader()
        );
    }

    private final DefaultHandler validationHandler = new DefaultHandler() {

        private boolean constraintsFileElement = false;

        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes) {

            if (CONSTRAINT_MAPPING_QNAME.equalsIgnoreCase(qName)) {
                constraintsFileElement = true;
            }
        }

        @Override
        public void characters(char[] ch, int start, int length) {

            if (constraintsFileElement) {
                constraintsFiles.add(new String(ch, start, length));
                constraintsFileElement = false;
            }
        }
    };

}
