/* * Copyright 2010, 2011 Christopher Pheby * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jadira.bindings.core.loader; import java.io.IOException; import java.io.InputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; import java.util.List; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.jadira.bindings.core.annotation.BindingScope; import org.jadira.bindings.core.annotation.DefaultBinding; import org.jadira.bindings.core.spi.ConverterProvider; import org.jadira.bindings.core.utils.lang.IterableNodeList; import org.jadira.bindings.core.utils.reflection.ClassLoaderUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.xml.sax.ErrorHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; /** * A class capable of reading configuration from a given URL and producing the * resultant {@link BindingConfiguration} representation */ public final class BindingXmlLoader { private static final String BINDINGS_NAMESPACE = "http://org.jadira.bindings/xml/ns/binding"; private BindingXmlLoader() { } /** * Given a configuration URL, produce the corresponding configuration * @param location The URL * @return The relevant {@link BindingConfiguration} * @throws IllegalStateException If the configuration cannot be parsed */ public static BindingConfiguration load(URL location) throws IllegalStateException { Document doc; try { doc = loadDocument(location); } catch (IOException e) { throw new IllegalStateException("Cannot load " + location.toExternalForm(), e); } catch (ParserConfigurationException e) { throw new IllegalStateException("Cannot initialise parser for " + location.toExternalForm(), e); } catch (SAXException e) { throw new IllegalStateException("Cannot parse " + location.toExternalForm(), e); } BindingConfiguration configuration = parseDocument(doc); return configuration; } /** * Helper method to load a DOM Document from the given configuration URL * @param location The configuration URL * @return A W3C DOM Document * @throws IOException If the configuration cannot be read * @throws ParserConfigurationException If the DOM Parser cannot be initialised * @throws SAXException If the configuraiton cannot be parsed */ private static Document loadDocument(URL location) throws IOException, ParserConfigurationException, SAXException { InputStream inputStream = null; if (location != null) { URLConnection urlConnection = location.openConnection(); urlConnection.setUseCaches(false); inputStream = urlConnection.getInputStream(); } if (inputStream == null) { if (location == null) { throw new IOException("Failed to obtain InputStream for named location: null"); } else { throw new IOException("Failed to obtain InputStream for named location: " + location.toExternalForm()); } } InputSource inputSource = new InputSource(inputStream); List<SAXParseException> errors = new ArrayList<SAXParseException>(); DocumentBuilder docBuilder = constructDocumentBuilder(errors); Document document = docBuilder.parse(inputSource); if (!errors.isEmpty()) { if (location == null) { throw new IllegalStateException("Invalid File: null", (Throwable) errors.get(0)); } else { throw new IllegalStateException("Invalid file: " + location.toExternalForm(), (Throwable) errors.get(0)); } } return document; } /** * Helper used to construct a document builder * @param errors A list for holding any errors that take place * @return JAXP {@link DocumentBuilder} * @throws ParserConfigurationException If the parser cannot be initialised */ private static DocumentBuilder constructDocumentBuilder(List<SAXParseException> errors) throws ParserConfigurationException { DocumentBuilderFactory documentBuilderFactory = constructDocumentBuilderFactory(); DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder(); docBuilder.setEntityResolver(new BindingXmlEntityResolver()); docBuilder.setErrorHandler(new BindingXmlErrorHandler(errors)); return docBuilder; } /** * Helper used to construct a {@link DocumentBuilderFactory} with schema validation configured * @return {@link DocumentBuilderFactory} */ private static DocumentBuilderFactory constructDocumentBuilderFactory() { DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); documentBuilderFactory.setValidating(true); documentBuilderFactory.setNamespaceAware(true); try { documentBuilderFactory.setAttribute("http://apache.org/xml/features/validation/schema", true); } catch (IllegalArgumentException e) { // Ignore } documentBuilderFactory.setAttribute("http://java.sun.com/xml/jaxp/properties/schemaLanguage", "http://www.w3.org/2001/XMLSchema"); documentBuilderFactory.setAttribute("http://java.sun.com/xml/jaxp/properties/schemaSource", "classpath:/jadira-bindings.xsd"); return documentBuilderFactory; } /** * Walk the parsed {@link Document} and produce a {@link BindingConfiguration} * @param doc Document being Parsed * @return The resultant {@link BindingConfiguration} */ private static BindingConfiguration parseDocument(Document doc) { BindingConfiguration result = new BindingConfiguration(); Element docRoot = doc.getDocumentElement(); for (Node next : new IterableNodeList(docRoot.getChildNodes())) { if (Node.ELEMENT_NODE == next.getNodeType()) { Element element = (Element) next; if (BINDINGS_NAMESPACE.equals(element.getNamespaceURI()) && "provider".equals(element.getLocalName())) { Provider provider = parseProviderElement(element); result.addProvider(provider); } if (BINDINGS_NAMESPACE.equals(element.getNamespaceURI()) && "extension".equals(element.getLocalName())) { Extension<?> extension = parseBinderExtensionElement(element); result.addExtension(extension); } if (BINDINGS_NAMESPACE.equals(element.getNamespaceURI()) && "binding".equals(element.getLocalName())) { BindingConfigurationEntry binding = parseBindingConfigurationEntryElement(element); result.addBindingEntry(binding); } } } return result; } /** * Parse the 'provider' element and its children * @param element The element * @return A {@link Provider} instance for the element */ private static Provider parseProviderElement(Element element) { Class<?> providerClass = lookupClass(element.getAttribute("class")); if (providerClass == null) { throw new IllegalStateException("Referenced class {" + element.getAttribute("class") + "} could not be found"); } if (!ConverterProvider.class.isAssignableFrom(providerClass)) { throw new IllegalStateException("Referenced class {" + element.getAttribute("class") + "} did not implement BindingProvider"); } @SuppressWarnings("unchecked") final Class<ConverterProvider> typedProviderClass = (Class<ConverterProvider>) providerClass; return new Provider((Class<ConverterProvider>) typedProviderClass); } /** * Parse the 'extension' element * @param element The element * @return A {@link Extension} instance for the element */ private static <T> Extension<T> parseBinderExtensionElement(Element element) { @SuppressWarnings("unchecked") Class<T> providerClass = (Class<T>)lookupClass(element.getAttribute("class")); Class<?> implementationClass = lookupClass(element.getAttribute("implementationClass")); if (providerClass == null) { throw new IllegalStateException("Referenced class {" + element.getAttribute("class") + "} could not be found"); } if (implementationClass == null) { throw new IllegalStateException("Referenced implementation class {" + element.getAttribute("implementationClass") + "} could not be found"); } if (providerClass.isAssignableFrom(implementationClass)) { throw new IllegalStateException("Referenced class {" + element.getAttribute("class") + "} did not implement BindingProvider"); } try { @SuppressWarnings("unchecked") final Class<? extends T> myImplementationClass = (Class<T>) implementationClass.newInstance(); return new Extension<T>(providerClass, myImplementationClass); } catch (InstantiationException e) { throw new IllegalStateException("Referenced implementation class {" + element.getAttribute("implementationClass") + "} could not be instantiated"); } catch (IllegalAccessException e) { throw new IllegalStateException("Referenced implementation class {" + element.getAttribute("implementationClass") + "} could not be accessed"); } } /** * Parse the {@link BindingConfigurationEntry} element * @param element The element * @return A {@link BindingConfigurationEntry} element */ @SuppressWarnings("unchecked") private static BindingConfigurationEntry parseBindingConfigurationEntryElement(Element element) { Class<?> bindingClass = null; Class<?> sourceClass = null; Class<?> targetClass = null; Method toMethod = null; Method fromMethod = null; Constructor<?> fromConstructor = null; Class<? extends Annotation> qualifier = DefaultBinding.class; if (element.getAttribute("class").length() > 0) { bindingClass = lookupClass(element.getAttribute("class")); } if (element.getAttribute("sourceClass").length() > 0) { sourceClass = lookupClass(element.getAttribute("sourceClass")); } if (element.getAttribute("targetClass").length() > 0) { targetClass = lookupClass(element.getAttribute("targetClass")); } if (element.getAttribute("qualifier").length() > 0) { qualifier = (Class<? extends Annotation>) lookupClass(element.getAttribute("qualifier")); if (qualifier.getAnnotation(BindingScope.class) == null) { throw new IllegalStateException("Qualifier class {" + element.getAttribute("qualifier") + "} was not marked as BindingScope"); } } if (bindingClass != null) { for (Node next : new IterableNodeList(element.getChildNodes())) { if (Node.ELEMENT_NODE == next.getNodeType()) { Element childElement = (Element) next; if (BINDINGS_NAMESPACE.equals(element.getNamespaceURI()) && "toMethod".equals(element.getLocalName())) { String toMethodName = childElement.getTextContent(); try { toMethod = bindingClass.getMethod(toMethodName, new Class[] { targetClass }); } catch (SecurityException e) { } catch (NoSuchMethodException e) { } if (toMethod != null && (!String.class.equals(toMethod.getReturnType()) || !Modifier.isStatic(toMethod.getModifiers()))) { toMethod = null; } if (toMethod == null && bindingClass.equals(targetClass)) { try { toMethod = bindingClass.getMethod(toMethodName, new Class[] {}); } catch (SecurityException e) { } catch (NoSuchMethodException e) { } if (toMethod != null && Modifier.isStatic(toMethod.getModifiers())) { toMethod = null; } } } else if (BINDINGS_NAMESPACE.equals(element.getNamespaceURI()) && "fromMethod".equals(element.getLocalName())) { String fromMethodName = childElement.getTextContent(); try { fromMethod = bindingClass.getMethod(fromMethodName, new Class[] { String.class }); } catch (SecurityException e) { } catch (NoSuchMethodException e) { } if (fromMethod != null && ((targetClass != null && !targetClass.isAssignableFrom(fromMethod.getReturnType())) || !Modifier.isStatic(fromMethod.getModifiers()))) { fromMethod = null; } } else if (BINDINGS_NAMESPACE.equals(element.getNamespaceURI()) && "fromConstructor".equals(element.getLocalName())) { try { fromConstructor = bindingClass.getConstructor(new Class[] { String.class }); } catch (SecurityException e) { } catch (NoSuchMethodException e) { } } } } } if (bindingClass == null) { if (sourceClass == null) { throw new IllegalStateException("If bindingClass is not populated, sourceClass must be present"); } if (targetClass == null) { throw new IllegalStateException("If bindingClass is not populated, targetClass must be present"); } if (fromMethod != null && fromConstructor != null) { throw new IllegalStateException("If fromMethod is populated, fromConstructor must not be present"); } if (fromMethod == null) { return new BindingConfigurationEntry(sourceClass, targetClass, qualifier, toMethod, fromConstructor); } else { return new BindingConfigurationEntry(sourceClass, targetClass, qualifier, toMethod, fromMethod); } } else { if (sourceClass != null) { throw new IllegalStateException("If bindingClass is populated, sourceClass must not be present"); } if (targetClass != null) { throw new IllegalStateException("If bindingClass is populated, targetClass must not be present"); } if (toMethod != null) { throw new IllegalStateException("If bindingClass is populated, toMethod must not be present"); } if (fromMethod != null) { throw new IllegalStateException("If bindingClass is populated, fromMethod must not be present"); } if (fromConstructor != null) { throw new IllegalStateException("If bindingClass is populated, fromConstructor must not be present"); } return new BindingConfigurationEntry(bindingClass, qualifier); } } /** * Helper method that given a class-name will create the appropriate Class instance * @param elementName The class name * @return Instance of Class */ private static Class<?> lookupClass(String elementName) { Class<?> clazz = null; try { clazz = ClassLoaderUtils.getClassLoader().loadClass(elementName); } catch (ClassNotFoundException e) { return null; } return clazz; } /** * SAX {@link ErrorHandler} that collects errors */ private static class BindingXmlErrorHandler implements ErrorHandler { private List<SAXParseException> errors; /** * Create a new instance with the given error list for collecting errors * @param errors Error list to use */ BindingXmlErrorHandler(List<SAXParseException> errors) { this.errors = errors; } /** * {@inheritDoc} */ /* @Override */ public void error(SAXParseException error) { errors.add(error); } /** * {@inheritDoc} */ /* @Override */ public void fatalError(SAXParseException error) { errors.add(error); } /** * {@inheritDoc} */ /* @Override */ public void warning(SAXParseException warn) { // ignore } } }