001/* 002 * (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others. 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 * 016 * Contributors: 017 * Nuxeo - initial API and implementation 018 */ 019package org.nuxeo.common.xmap; 020 021import static java.nio.charset.StandardCharsets.UTF_8; 022 023import java.io.File; 024import java.io.IOException; 025import java.io.InputStream; 026import java.io.OutputStream; 027import java.lang.annotation.Annotation; 028import java.lang.reflect.AnnotatedElement; 029import java.lang.reflect.Field; 030import java.lang.reflect.Method; 031import java.net.URL; 032import java.util.ArrayList; 033import java.util.Collection; 034import java.util.Hashtable; 035import java.util.List; 036import java.util.Map; 037 038import javax.xml.parsers.DocumentBuilder; 039import javax.xml.parsers.DocumentBuilderFactory; 040import javax.xml.parsers.ParserConfigurationException; 041 042import org.apache.commons.io.FileUtils; 043import org.nuxeo.common.xmap.annotation.XContent; 044import org.nuxeo.common.xmap.annotation.XContext; 045import org.nuxeo.common.xmap.annotation.XMemberAnnotation; 046import org.nuxeo.common.xmap.annotation.XNode; 047import org.nuxeo.common.xmap.annotation.XNodeList; 048import org.nuxeo.common.xmap.annotation.XNodeMap; 049import org.nuxeo.common.xmap.annotation.XObject; 050import org.nuxeo.common.xmap.annotation.XParent; 051import org.w3c.dom.Document; 052import org.w3c.dom.Element; 053import org.w3c.dom.Node; 054import org.xml.sax.SAXException; 055 056/** 057 * XMap maps an XML file to a java object. 058 * <p> 059 * The mapping is described by annotations on java objects. 060 * <p> 061 * The following annotations are supported: 062 * <ul> 063 * <li> {@link XObject} Mark the object as being mappable to an XML node 064 * <li> {@link XNode} Map an XML node to a field of a mappable object 065 * <li> {@link XNodeList} Map an list of XML nodes to a field of a mappable object 066 * <li> {@link XNodeMap} Map an map of XML nodes to a field of a mappable object 067 * <li> {@link XContent} Map an XML node content to a field of a mappable object 068 * <li> {@link XParent} Map a field of the current mappable object to the parent object if any exists The parent object 069 * is the mappable object containing the current object as a field 070 * </ul> 071 * The mapping is done in 2 steps: 072 * <ul> 073 * <li>The XML file is loaded as a DOM document 074 * <li>The DOM document is parsed and the nodes mapping is resolved 075 * </ul> 076 * 077 * @author <a href="mailto:[email protected]">Bogdan Stefanescu</a> 078 */ 079@SuppressWarnings({ "SuppressionAnnotation" }) 080public class XMap { 081 082 private static DocumentBuilderFactory initFactory() { 083 Thread t = Thread.currentThread(); 084 ClassLoader cl = t.getContextClassLoader(); 085 t.setContextClassLoader(XMap.class.getClassLoader()); 086 try { 087 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 088 factory.setNamespaceAware(true); 089 return factory; 090 } finally { 091 t.setContextClassLoader(cl); 092 } 093 } 094 095 public static DocumentBuilderFactory getFactory() { 096 return factory; 097 } 098 099 private static DocumentBuilderFactory factory = initFactory(); 100 101 // top level objects 102 private final Map<String, XAnnotatedObject> roots; 103 104 // the scanned objects 105 private final Map<Class<?>, XAnnotatedObject> objects; 106 107 private final Map<Class<?>, XValueFactory> factories; 108 109 /** 110 * Creates a new XMap object. 111 */ 112 public XMap() { 113 objects = new Hashtable<>(); 114 roots = new Hashtable<>(); 115 factories = new Hashtable<>(XValueFactory.defaultFactories); 116 } 117 118 /** 119 * Gets the value factory used for objects of the given class. 120 * <p> 121 * Value factories are used to decode values from XML strings. 122 * 123 * @param type the object type 124 * @return the value factory if any, null otherwise 125 */ 126 public XValueFactory getValueFactory(Class<?> type) { 127 return factories.get(type); 128 } 129 130 /** 131 * Sets a custom value factory for the given class. 132 * <p> 133 * Value factories are used to decode values from XML strings. 134 * 135 * @param type the object type 136 * @param factory the value factory to use for the given type 137 */ 138 public void setValueFactory(Class<?> type, XValueFactory factory) { 139 factories.put(type, factory); 140 } 141 142 /** 143 * Gets a list of scanned objects. 144 * <p> 145 * Scanned objects are annotated objects that were registered by this XMap instance. 146 */ 147 public Collection<XAnnotatedObject> getScannedObjects() { 148 return objects.values(); 149 } 150 151 /** 152 * Gets the root objects. 153 * <p> 154 * Root objects are scanned objects that can be mapped to XML elements that are not part from other objects. 155 * 156 * @return the root objects 157 */ 158 public Collection<XAnnotatedObject> getRootObjects() { 159 return roots.values(); 160 } 161 162 /** 163 * Registers a mappable object class. 164 * <p> 165 * The class will be scanned for XMap annotations and a mapping description is created. 166 * 167 * @param klass the object class 168 * @return the mapping description 169 */ 170 public XAnnotatedObject register(Class<?> klass) { 171 XAnnotatedObject xao = objects.get(klass); 172 if (xao == null) { // avoid scanning twice 173 XObject xob = checkObjectAnnotation(klass); 174 if (xob != null) { 175 xao = new XAnnotatedObject(this, klass, xob); 176 objects.put(xao.klass, xao); 177 scan(xao); 178 String key = xob.value(); 179 if (key.length() > 0) { 180 roots.put(xao.path.path, xao); 181 } 182 } 183 } 184 return xao; 185 } 186 187 private void scan(XAnnotatedObject xob) { 188 scanClass(xob, xob.klass); 189 } 190 191 private void scanClass(XAnnotatedObject xob, Class<?> aClass) { 192 Field[] fields = aClass.getDeclaredFields(); 193 for (Field field : fields) { 194 Annotation anno = checkMemberAnnotation(field); 195 if (anno != null) { 196 XAnnotatedMember member = createFieldMember(field, anno); 197 xob.addMember(member); 198 } 199 } 200 201 Method[] methods = aClass.getDeclaredMethods(); 202 for (Method method : methods) { 203 // we accept only methods with one parameter 204 Class<?>[] paramTypes = method.getParameterTypes(); 205 if (paramTypes.length != 1) { 206 continue; 207 } 208 Annotation anno = checkMemberAnnotation(method); 209 if (anno != null) { 210 XAnnotatedMember member = createMethodMember(method, anno, aClass); 211 xob.addMember(member); 212 } 213 } 214 215 // scan superClass annotations 216 if (aClass.getSuperclass() != null) { 217 scanClass(xob, aClass.getSuperclass()); 218 } 219 } 220 221 /** 222 * Processes the XML file at the given URL using a default context. 223 * 224 * @param url the XML file url 225 * @return the first registered top level object that is found in the file, or null if no objects are found. 226 */ 227 public Object load(URL url) throws IOException { 228 return load(new Context(), url.openStream()); 229 } 230 231 /** 232 * Processes the XML file at the given URL and using the given contexts. 233 * 234 * @param ctx the context to use 235 * @param url the XML file url 236 * @return the first registered top level object that is found in the file. 237 */ 238 public Object load(Context ctx, URL url) throws IOException { 239 return load(ctx, url.openStream()); 240 } 241 242 /** 243 * Processes the XML content from the given input stream using a default context. 244 * 245 * @param in the XML input source 246 * @return the first registered top level object that is found in the file. 247 */ 248 public Object load(InputStream in) throws IOException { 249 return load(new Context(), in); 250 } 251 252 /** 253 * Processes the XML content from the given input stream using the given context. 254 * 255 * @param ctx the context to use 256 * @param in the input stream 257 * @return the first registered top level object that is found in the file. 258 */ 259 public Object load(Context ctx, InputStream in) throws IOException { 260 try { 261 DocumentBuilderFactory factory = getFactory(); 262 DocumentBuilder builder = factory.newDocumentBuilder(); 263 Document document = builder.parse(in); 264 return load(ctx, document.getDocumentElement()); 265 } catch (ParserConfigurationException | SAXException e) { 266 throw new IOException(e); 267 } finally { 268 if (in != null) { 269 try { 270 in.close(); 271 } catch (IOException e) { 272 // do nothing 273 } 274 } 275 } 276 } 277 278 /** 279 * Processes the XML file at the given URL using a default context. 280 * <p> 281 * Returns a list with all registered top level objects that are found in the file. 282 * <p> 283 * If not objects are found, an empty list is returned. 284 * 285 * @param url the XML file url 286 * @return a list with all registered top level objects that are found in the file 287 */ 288 public Object[] loadAll(URL url) throws IOException { 289 return loadAll(new Context(), url.openStream()); 290 } 291 292 /** 293 * Processes the XML file at the given URL using the given context 294 * <p> 295 * Return a list with all registered top level objects that are found in the file. 296 * <p> 297 * If not objects are found an empty list is retoruned. 298 * 299 * @param ctx the context to use 300 * @param url the XML file url 301 * @return a list with all registered top level objects that are found in the file 302 */ 303 public Object[] loadAll(Context ctx, URL url) throws IOException { 304 return loadAll(ctx, url.openStream()); 305 } 306 307 /** 308 * Processes the XML from the given input stream using the given context. 309 * <p> 310 * Returns a list with all registered top level objects that are found in the file. 311 * <p> 312 * If not objects are found, an empty list is returned. 313 * 314 * @param in the XML input stream 315 * @return a list with all registered top level objects that are found in the file 316 */ 317 public Object[] loadAll(InputStream in) throws IOException { 318 return loadAll(new Context(), in); 319 } 320 321 /** 322 * Processes the XML from the given input stream using the given context. 323 * <p> 324 * Returns a list with all registered top level objects that are found in the file. 325 * <p> 326 * If not objects are found, an empty list is returned. 327 * 328 * @param ctx the context to use 329 * @param in the XML input stream 330 * @return a list with all registered top level objects that are found in the file 331 */ 332 public Object[] loadAll(Context ctx, InputStream in) throws IOException { 333 try { 334 DocumentBuilderFactory factory = getFactory(); 335 DocumentBuilder builder = factory.newDocumentBuilder(); 336 Document document = builder.parse(in); 337 return loadAll(ctx, document.getDocumentElement()); 338 } catch (ParserConfigurationException | SAXException e) { 339 throw new IOException(e); 340 } finally { 341 if (in != null) { 342 try { 343 in.close(); 344 } catch (IOException e) { 345 // do nothing 346 } 347 } 348 } 349 } 350 351 /** 352 * Processes the given DOM element and return the first mappable object found in the element. 353 * <p> 354 * A default context is used. 355 * 356 * @param root the element to process 357 * @return the first object found in this element or null if none 358 */ 359 public Object load(Element root) { 360 return load(new Context(), root); 361 } 362 363 /** 364 * Processes the given DOM element and return the first mappable object found in the element. 365 * <p> 366 * The given context is used. 367 * 368 * @param ctx the context to use 369 * @param root the element to process 370 * @return the first object found in this element or null if none 371 */ 372 public Object load(Context ctx, Element root) { 373 // check if the current element is bound to an annotated object 374 String name = root.getNodeName(); 375 XAnnotatedObject xob = roots.get(name); 376 if (xob != null) { 377 return xob.newInstance(ctx, root); 378 } else { 379 Node p = root.getFirstChild(); 380 while (p != null) { 381 if (p.getNodeType() == Node.ELEMENT_NODE) { 382 // Recurse in the first child Element 383 return load((Element) p); 384 } 385 p = p.getNextSibling(); 386 } 387 // We didn't find any Element 388 return null; 389 } 390 } 391 392 /** 393 * Processes the given DOM element and return a list with all top-level mappable objects found in the element. 394 * <p> 395 * The given context is used. 396 * 397 * @param ctx the context to use 398 * @param root the element to process 399 * @return the list of all top level objects found 400 */ 401 public Object[] loadAll(Context ctx, Element root) { 402 List<Object> result = new ArrayList<>(); 403 loadAll(ctx, root, result); 404 return result.toArray(); 405 } 406 407 /** 408 * Processes the given DOM element and return a list with all top-level mappable objects found in the element. 409 * <p> 410 * The default context is used. 411 * 412 * @param root the element to process 413 * @return the list of all top level objects found 414 */ 415 public Object[] loadAll(Element root) { 416 return loadAll(new Context(), root); 417 } 418 419 /** 420 * Same as {@link XMap#loadAll(Element)} but put collected objects in the given collection. 421 * 422 * @param root the element to process 423 * @param result the collection where to collect objects 424 */ 425 public void loadAll(Element root, Collection<Object> result) { 426 loadAll(new Context(), root, result); 427 } 428 429 /** 430 * Same as {@link XMap#loadAll(Context, Element)} but put collected objects in the given collection. 431 * 432 * @param ctx the context to use 433 * @param root the element to process 434 * @param result the collection where to collect objects 435 */ 436 public void loadAll(Context ctx, Element root, Collection<Object> result) { 437 // check if the current element is bound to an annotated object 438 String name = root.getNodeName(); 439 XAnnotatedObject xob = roots.get(name); 440 if (xob != null) { 441 Object ob = xob.newInstance(ctx, root); 442 result.add(ob); 443 } else { 444 Node p = root.getFirstChild(); 445 while (p != null) { 446 if (p.getNodeType() == Node.ELEMENT_NODE) { 447 loadAll(ctx, (Element) p, result); 448 } 449 p = p.getNextSibling(); 450 } 451 } 452 } 453 454 protected static Annotation checkMemberAnnotation(AnnotatedElement ae) { 455 Annotation[] annos = ae.getAnnotations(); 456 for (Annotation anno : annos) { 457 if (anno.annotationType().isAnnotationPresent(XMemberAnnotation.class)) { 458 return anno; 459 } 460 } 461 return null; 462 } 463 464 protected static XObject checkObjectAnnotation(AnnotatedElement ae) { 465 return ae.getAnnotation(XObject.class); 466 } 467 468 private XAnnotatedMember createMember(Annotation annotation, XAccessor setter) { 469 XAnnotatedMember member = null; 470 int type = annotation.annotationType().getAnnotation(XMemberAnnotation.class).value(); 471 if (type == XMemberAnnotation.NODE) { 472 member = new XAnnotatedMember(this, setter, (XNode) annotation); 473 } else if (type == XMemberAnnotation.NODE_LIST) { 474 member = new XAnnotatedList(this, setter, (XNodeList) annotation); 475 } else if (type == XMemberAnnotation.NODE_MAP) { 476 member = new XAnnotatedMap(this, setter, (XNodeMap) annotation); 477 } else if (type == XMemberAnnotation.PARENT) { 478 member = new XAnnotatedParent(this, setter); 479 } else if (type == XMemberAnnotation.CONTENT) { 480 member = new XAnnotatedContent(this, setter, (XContent) annotation); 481 } else if (type == XMemberAnnotation.CONTEXT) { 482 member = new XAnnotatedContext(this, setter, (XContext) annotation); 483 } 484 return member; 485 } 486 487 public final XAnnotatedMember createFieldMember(Field field, Annotation annotation) { 488 XAccessor setter = new XFieldAccessor(field); 489 return createMember(annotation, setter); 490 } 491 492 public final XAnnotatedMember createMethodMember(Method method, Annotation annotation, Class<?> klass) { 493 XAccessor setter = new XMethodAccessor(method, klass); 494 return createMember(annotation, setter); 495 } 496 497 // methods to serialize the map 498 public String toXML(Object object) throws IOException { 499 DocumentBuilderFactory dbfac = getFactory(); 500 DocumentBuilder docBuilder; 501 try { 502 docBuilder = dbfac.newDocumentBuilder(); 503 } catch (ParserConfigurationException e) { 504 throw new IOException(e); 505 } 506 Document doc = docBuilder.newDocument(); 507 // create root element 508 Element root = doc.createElement("root"); 509 doc.appendChild(root); 510 511 // load xml reprezentation in root 512 toXML(object, root); 513 return DOMSerializer.toString(root); 514 } 515 516 public void toXML(Object object, OutputStream os) throws IOException { 517 String xml = toXML(object); 518 os.write(xml.getBytes()); 519 } 520 521 public void toXML(Object object, File file) throws IOException { 522 String xml = toXML(object); 523 FileUtils.writeStringToFile(file, xml, UTF_8); 524 } 525 526 public void toXML(Object object, Element root) { 527 XAnnotatedObject xao = objects.get(object.getClass()); 528 if (xao == null) { 529 throw new IllegalArgumentException(object.getClass().getCanonicalName() + " is NOT registred in xmap"); 530 } 531 XMLBuilder.saveToXML(object, root, xao); 532 } 533 534}