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