View Javadoc
1   /*
2    *  Copyright 2010, 2011 Christopher Pheby
3    *
4    *  Licensed under the Apache License, Version 2.0 (the "License");
5    *  you may not use this file except in compliance with the License.
6    *  You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *  Unless required by applicable law or agreed to in writing, software
11   *  distributed under the License is distributed on an "AS IS" BASIS,
12   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *  See the License for the specific language governing permissions and
14   *  limitations under the License.
15   */
16  package org.jadira.bindings.core.loader;
17  
18  import java.io.IOException;
19  import java.io.InputStream;
20  import java.lang.annotation.Annotation;
21  import java.lang.reflect.Constructor;
22  import java.lang.reflect.Method;
23  import java.lang.reflect.Modifier;
24  import java.net.URL;
25  import java.net.URLConnection;
26  import java.util.ArrayList;
27  import java.util.List;
28  
29  import javax.xml.parsers.DocumentBuilder;
30  import javax.xml.parsers.DocumentBuilderFactory;
31  import javax.xml.parsers.ParserConfigurationException;
32  
33  import org.jadira.bindings.core.annotation.BindingScope;
34  import org.jadira.bindings.core.annotation.DefaultBinding;
35  import org.jadira.bindings.core.spi.ConverterProvider;
36  import org.jadira.bindings.core.utils.lang.IterableNodeList;
37  import org.jadira.bindings.core.utils.reflection.ClassLoaderUtils;
38  import org.w3c.dom.Document;
39  import org.w3c.dom.Element;
40  import org.w3c.dom.Node;
41  import org.xml.sax.ErrorHandler;
42  import org.xml.sax.InputSource;
43  import org.xml.sax.SAXException;
44  import org.xml.sax.SAXParseException;
45  
46  /**
47   * A class capable of reading configuration from a given URL and producing the
48   * resultant {@link BindingConfiguration} representation
49   */
50  public final class BindingXmlLoader {
51  
52      private static final String BINDINGS_NAMESPACE = "http://org.jadira.bindings/xml/ns/binding";
53  
54  	private BindingXmlLoader() {
55      }
56  
57      /**
58       * Given a configuration URL, produce the corresponding configuration
59       * @param location The URL
60       * @return The relevant {@link BindingConfiguration}
61       * @throws IllegalStateException If the configuration cannot be parsed
62       */
63      public static BindingConfiguration load(URL location) throws IllegalStateException {
64      	
65          Document doc;
66          try {
67              doc = loadDocument(location);
68          } catch (IOException e) {
69              throw new IllegalStateException("Cannot load " + location.toExternalForm(), e);
70          } catch (ParserConfigurationException e) {
71              throw new IllegalStateException("Cannot initialise parser for " + location.toExternalForm(), e);
72          } catch (SAXException e) {
73              throw new IllegalStateException("Cannot parse " + location.toExternalForm(), e);
74          }
75          BindingConfiguration configuration = parseDocument(doc);
76          return configuration;
77      }
78  
79      /**
80       * Helper method to load a DOM Document from the given configuration URL
81       * @param location The configuration URL
82       * @return A W3C DOM Document
83       * @throws IOException If the configuration cannot be read
84       * @throws ParserConfigurationException If the DOM Parser cannot be initialised
85       * @throws SAXException If the configuraiton cannot be parsed
86       */
87      private static Document loadDocument(URL location) throws IOException, ParserConfigurationException, SAXException {
88  
89          InputStream inputStream = null;
90  
91          if (location != null) {
92              URLConnection urlConnection = location.openConnection();
93              urlConnection.setUseCaches(false);
94              inputStream = urlConnection.getInputStream();
95          }
96          if (inputStream == null) {
97          	if (location == null) {
98          		throw new IOException("Failed to obtain InputStream for named location: null");
99          	} 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 }