001/*
002 *  Copyright 2010, 2011 Christopher Pheby
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.jadira.bindings.core.loader;
017
018import java.io.IOException;
019import java.io.InputStream;
020import java.lang.annotation.Annotation;
021import java.lang.reflect.Constructor;
022import java.lang.reflect.Method;
023import java.lang.reflect.Modifier;
024import java.net.URL;
025import java.net.URLConnection;
026import java.util.ArrayList;
027import java.util.List;
028
029import javax.xml.parsers.DocumentBuilder;
030import javax.xml.parsers.DocumentBuilderFactory;
031import javax.xml.parsers.ParserConfigurationException;
032
033import org.jadira.bindings.core.annotation.BindingScope;
034import org.jadira.bindings.core.annotation.DefaultBinding;
035import org.jadira.bindings.core.spi.ConverterProvider;
036import org.jadira.bindings.core.utils.lang.IterableNodeList;
037import org.jadira.bindings.core.utils.reflection.ClassLoaderUtils;
038import org.w3c.dom.Document;
039import org.w3c.dom.Element;
040import org.w3c.dom.Node;
041import org.xml.sax.ErrorHandler;
042import org.xml.sax.InputSource;
043import org.xml.sax.SAXException;
044import org.xml.sax.SAXParseException;
045
046/**
047 * A class capable of reading configuration from a given URL and producing the
048 * resultant {@link BindingConfiguration} representation
049 */
050public final class BindingXmlLoader {
051
052    private static final String BINDINGS_NAMESPACE = "http://org.jadira.bindings/xml/ns/binding";
053
054        private BindingXmlLoader() {
055    }
056
057    /**
058     * Given a configuration URL, produce the corresponding configuration
059     * @param location The URL
060     * @return The relevant {@link BindingConfiguration}
061     * @throws IllegalStateException If the configuration cannot be parsed
062     */
063    public static BindingConfiguration load(URL location) throws IllegalStateException {
064        
065        Document doc;
066        try {
067            doc = loadDocument(location);
068        } catch (IOException e) {
069            throw new IllegalStateException("Cannot load " + location.toExternalForm(), e);
070        } catch (ParserConfigurationException e) {
071            throw new IllegalStateException("Cannot initialise parser for " + location.toExternalForm(), e);
072        } catch (SAXException e) {
073            throw new IllegalStateException("Cannot parse " + location.toExternalForm(), e);
074        }
075        BindingConfiguration configuration = parseDocument(doc);
076        return configuration;
077    }
078
079    /**
080     * Helper method to load a DOM Document from the given configuration URL
081     * @param location The configuration URL
082     * @return A W3C DOM Document
083     * @throws IOException If the configuration cannot be read
084     * @throws ParserConfigurationException If the DOM Parser cannot be initialised
085     * @throws SAXException If the configuraiton cannot be parsed
086     */
087    private static Document loadDocument(URL location) throws IOException, ParserConfigurationException, SAXException {
088
089        InputStream inputStream = null;
090
091        if (location != null) {
092            URLConnection urlConnection = location.openConnection();
093            urlConnection.setUseCaches(false);
094            inputStream = urlConnection.getInputStream();
095        }
096        if (inputStream == null) {
097                if (location == null) {
098                        throw new IOException("Failed to obtain InputStream for named location: null");
099                } else {
100                        throw new IOException("Failed to obtain InputStream for named location: " + location.toExternalForm());
101                }
102        }
103
104        InputSource inputSource = new InputSource(inputStream);
105
106        List<SAXParseException> errors = new ArrayList<SAXParseException>();
107        DocumentBuilder docBuilder = constructDocumentBuilder(errors);
108
109        Document document = docBuilder.parse(inputSource);
110        if (!errors.isEmpty()) {
111                if (location == null) {
112                        throw new IllegalStateException("Invalid File: null", (Throwable) errors.get(0));
113                } else {
114                        throw new IllegalStateException("Invalid file: " + location.toExternalForm(), (Throwable) errors.get(0));
115                }
116        }
117        return document;
118    }
119
120    /**
121     * Helper used to construct a document builder
122     * @param errors A list for holding any errors that take place
123     * @return JAXP {@link DocumentBuilder}
124     * @throws ParserConfigurationException If the parser cannot be initialised
125     */
126    private static DocumentBuilder constructDocumentBuilder(List<SAXParseException> errors)
127            throws ParserConfigurationException {
128
129        DocumentBuilderFactory documentBuilderFactory = constructDocumentBuilderFactory();
130        DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder();
131        docBuilder.setEntityResolver(new BindingXmlEntityResolver());
132        docBuilder.setErrorHandler(new BindingXmlErrorHandler(errors));
133        return docBuilder;
134    }
135
136    /**
137     * Helper used to construct a {@link DocumentBuilderFactory} with schema validation configured
138     * @return {@link DocumentBuilderFactory}
139     */
140    private static DocumentBuilderFactory constructDocumentBuilderFactory() {
141
142        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
143
144        documentBuilderFactory.setValidating(true);
145        documentBuilderFactory.setNamespaceAware(true);
146
147        try {
148            documentBuilderFactory.setAttribute("http://apache.org/xml/features/validation/schema", true);
149        } catch (IllegalArgumentException e) {
150            // Ignore
151        }
152        documentBuilderFactory.setAttribute("http://java.sun.com/xml/jaxp/properties/schemaLanguage",
153                "http://www.w3.org/2001/XMLSchema");
154        documentBuilderFactory.setAttribute("http://java.sun.com/xml/jaxp/properties/schemaSource",
155                "classpath:/jadira-bindings.xsd");
156        return documentBuilderFactory;
157    }
158
159    /**
160     * Walk the parsed {@link Document} and produce a {@link BindingConfiguration}
161     * @param doc Document being Parsed
162     * @return The resultant {@link BindingConfiguration}
163     */
164    private static BindingConfiguration parseDocument(Document doc) {
165
166        BindingConfiguration result = new BindingConfiguration();
167
168        Element docRoot = doc.getDocumentElement();
169        for (Node next : new IterableNodeList(docRoot.getChildNodes())) {
170            if (Node.ELEMENT_NODE == next.getNodeType()) {
171                Element element = (Element) next;
172
173                if (BINDINGS_NAMESPACE.equals(element.getNamespaceURI())
174                        && "provider".equals(element.getLocalName())) {
175
176                    Provider provider = parseProviderElement(element);
177                    result.addProvider(provider);
178                }
179
180                if (BINDINGS_NAMESPACE.equals(element.getNamespaceURI())
181                        && "extension".equals(element.getLocalName())) {
182
183                    Extension<?> extension = parseBinderExtensionElement(element);
184                    result.addExtension(extension);
185                }
186                
187                if (BINDINGS_NAMESPACE.equals(element.getNamespaceURI())
188                        && "binding".equals(element.getLocalName())) {
189
190                    BindingConfigurationEntry binding = parseBindingConfigurationEntryElement(element);
191                    result.addBindingEntry(binding);
192                }
193            }
194        }
195        return result;
196    }
197
198    /**
199     * Parse the 'provider' element and its children
200     * @param element The element
201     * @return A {@link Provider} instance for the element
202     */
203    private static Provider parseProviderElement(Element element) {
204
205        Class<?> providerClass = lookupClass(element.getAttribute("class"));
206
207        if (providerClass == null) {
208            throw new IllegalStateException("Referenced class {" + element.getAttribute("class")
209                    + "} could not be found");
210        }
211        if (!ConverterProvider.class.isAssignableFrom(providerClass)) {
212            throw new IllegalStateException("Referenced class {" + element.getAttribute("class")
213                    + "} did not implement BindingProvider");
214        }
215
216        @SuppressWarnings("unchecked")
217        final Class<ConverterProvider> typedProviderClass = (Class<ConverterProvider>) providerClass;
218        return new Provider((Class<ConverterProvider>) typedProviderClass);
219    }
220
221    /**
222     * Parse the 'extension' element
223     * @param element The element
224     * @return A {@link Extension} instance for the element
225     */
226    private static <T> Extension<T> parseBinderExtensionElement(Element element) {
227
228        @SuppressWarnings("unchecked")
229                Class<T> providerClass = (Class<T>)lookupClass(element.getAttribute("class"));
230        
231        Class<?> implementationClass = lookupClass(element.getAttribute("implementationClass"));
232
233        if (providerClass == null) {
234            throw new IllegalStateException("Referenced class {" + element.getAttribute("class")
235                    + "} could not be found");
236        }
237        if (implementationClass == null) {
238            throw new IllegalStateException("Referenced implementation class {" + element.getAttribute("implementationClass")
239                    + "} could not be found");
240        }
241        if (providerClass.isAssignableFrom(implementationClass)) {
242            throw new IllegalStateException("Referenced class {" + element.getAttribute("class")
243                    + "} did not implement BindingProvider");
244        }
245
246                try {
247                        @SuppressWarnings("unchecked")
248                        final Class<? extends T> myImplementationClass = (Class<T>) implementationClass.newInstance();
249                        return new Extension<T>(providerClass, myImplementationClass);
250                } catch (InstantiationException e) {
251            throw new IllegalStateException("Referenced implementation class {" + element.getAttribute("implementationClass")
252                    + "} could not be instantiated");
253                } catch (IllegalAccessException e) {
254            throw new IllegalStateException("Referenced implementation class {" + element.getAttribute("implementationClass")
255                    + "} could not be accessed");
256                }
257    }
258    
259    /**
260     * Parse the {@link BindingConfigurationEntry} element
261     * @param element The element
262     * @return A {@link BindingConfigurationEntry} element
263     */
264    @SuppressWarnings("unchecked")
265        private static BindingConfigurationEntry parseBindingConfigurationEntryElement(Element element) {
266
267        Class<?> bindingClass = null;
268        Class<?> sourceClass = null;
269        Class<?> targetClass = null;
270        Method toMethod = null;
271        Method fromMethod = null;
272        Constructor<?> fromConstructor = null;
273        Class<? extends Annotation> qualifier = DefaultBinding.class;
274        
275        if (element.getAttribute("class").length() > 0) {
276                bindingClass = lookupClass(element.getAttribute("class"));
277        }
278        
279        if (element.getAttribute("sourceClass").length() > 0) {
280            sourceClass = lookupClass(element.getAttribute("sourceClass"));
281        }
282        if (element.getAttribute("targetClass").length() > 0) {
283                targetClass = lookupClass(element.getAttribute("targetClass"));
284        }
285
286        if (element.getAttribute("qualifier").length() > 0) {
287                
288            qualifier = (Class<? extends Annotation>) lookupClass(element.getAttribute("qualifier"));
289
290            if (qualifier.getAnnotation(BindingScope.class) == null) {
291                    throw new IllegalStateException("Qualifier class {" + element.getAttribute("qualifier")
292                            + "} was not marked as BindingScope");
293            }
294        }
295
296        if (bindingClass != null) {
297                for (Node next : new IterableNodeList(element.getChildNodes())) {
298                    if (Node.ELEMENT_NODE == next.getNodeType()) {
299                        Element childElement = (Element) next;
300                        if (BINDINGS_NAMESPACE.equals(element.getNamespaceURI())
301                                && "toMethod".equals(element.getLocalName())) {
302        
303                            String toMethodName = childElement.getTextContent();
304        
305                            try {
306                                toMethod = bindingClass.getMethod(toMethodName, new Class[] { targetClass });
307                            } catch (SecurityException e) {
308                            } catch (NoSuchMethodException e) {
309                            }
310                            if (toMethod != null && (!String.class.equals(toMethod.getReturnType())
311                                    || !Modifier.isStatic(toMethod.getModifiers()))) {
312                                toMethod = null;
313                            }
314                            if (toMethod == null && bindingClass.equals(targetClass)) {
315                                try {
316                                    toMethod = bindingClass.getMethod(toMethodName, new Class[] {});
317                                } catch (SecurityException e) {
318                                } catch (NoSuchMethodException e) {
319                                }
320                                if (toMethod != null && Modifier.isStatic(toMethod.getModifiers())) {
321                                    toMethod = null;
322                                }
323                            }
324        
325                        } else if (BINDINGS_NAMESPACE.equals(element.getNamespaceURI())
326                                && "fromMethod".equals(element.getLocalName())) {
327        
328                            String fromMethodName = childElement.getTextContent();
329        
330                            try {
331                                fromMethod = bindingClass.getMethod(fromMethodName, new Class[] { String.class });
332                            } catch (SecurityException e) {
333                            } catch (NoSuchMethodException e) {
334                            }
335                            if (fromMethod != null && ((targetClass != null && !targetClass.isAssignableFrom(fromMethod.getReturnType()))
336                                || !Modifier.isStatic(fromMethod.getModifiers()))) {
337                                fromMethod = null;
338                            }
339        
340                        } else if (BINDINGS_NAMESPACE.equals(element.getNamespaceURI())
341                                && "fromConstructor".equals(element.getLocalName())) {
342        
343                            try {
344                                fromConstructor = bindingClass.getConstructor(new Class[] { String.class });
345                            } catch (SecurityException e) {
346                            } catch (NoSuchMethodException e) {
347                            }
348                        }
349                    }
350                }
351        }
352
353        if (bindingClass == null) {
354
355                if (sourceClass == null) {
356                        throw new IllegalStateException("If bindingClass is not populated, sourceClass must be present");
357                }
358                if (targetClass == null) {
359                        throw new IllegalStateException("If bindingClass is not populated, targetClass must be present");
360                }
361                if (fromMethod != null && fromConstructor != null) {
362                        throw new IllegalStateException("If fromMethod is populated, fromConstructor must not be present");
363                }
364
365                if (fromMethod == null) {
366                
367                        return new BindingConfigurationEntry(sourceClass, targetClass, qualifier, toMethod, fromConstructor);
368                } else {
369                        
370                        return new BindingConfigurationEntry(sourceClass, targetClass, qualifier, toMethod, fromMethod);
371                }
372        } else {
373                if (sourceClass != null) {
374                        throw new IllegalStateException("If bindingClass is populated, sourceClass must not be present");
375                }
376                if (targetClass != null) {
377                        throw new IllegalStateException("If bindingClass is populated, targetClass must not be present");
378                }
379                if (toMethod != null) {
380                        throw new IllegalStateException("If bindingClass is populated, toMethod must not be present");
381                }
382                if (fromMethod != null) {
383                        throw new IllegalStateException("If bindingClass is populated, fromMethod must not be present");
384                }
385                if (fromConstructor != null) {
386                        throw new IllegalStateException("If bindingClass is populated, fromConstructor must not be present");
387                }
388                
389                return new BindingConfigurationEntry(bindingClass, qualifier);
390        }
391    }
392
393    /**
394     * Helper method that given a class-name will create the appropriate Class instance
395     * @param elementName The class name
396     * @return Instance of Class
397     */
398    private static Class<?> lookupClass(String elementName) {
399
400        Class<?> clazz = null;
401        try {
402            clazz = ClassLoaderUtils.getClassLoader().loadClass(elementName);
403        } catch (ClassNotFoundException e) {
404            return null;
405        }
406
407        return clazz;
408    }
409
410    /**
411     * SAX {@link ErrorHandler} that collects errors
412     */
413    private static class BindingXmlErrorHandler implements ErrorHandler {
414
415        private List<SAXParseException> errors;
416
417        /**
418         * Create a new instance with the given error list for collecting errors
419         * @param errors Error list to use
420         */
421        BindingXmlErrorHandler(List<SAXParseException> errors) {
422            this.errors = errors;
423        }
424
425        /**
426         * {@inheritDoc}
427         */
428        /* @Override */
429        public void error(SAXParseException error) {
430            errors.add(error);
431        }
432
433        /**
434         * {@inheritDoc}
435         */
436        /* @Override */
437        public void fatalError(SAXParseException error) {
438            errors.add(error);
439        }
440
441        /**
442         * {@inheritDoc}
443         */
444        /* @Override */
445        public void warning(SAXParseException warn) {
446            // ignore
447        }
448    }
449}