blob: e7e049674ca560b7dcfe7eda8d441a394112bbfd [file] [log] [blame]
/*
* Copyright (c) 2016, 2020 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;
}
}
};
}