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