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}