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