001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019 020package org.apache.isis.core.runtime.snapshot; 021 022import java.io.StringReader; 023import java.io.StringWriter; 024import java.util.Collections; 025import java.util.Enumeration; 026import java.util.List; 027import java.util.Map; 028import java.util.StringTokenizer; 029import java.util.UUID; 030import java.util.Vector; 031 032import javax.xml.parsers.DocumentBuilder; 033import javax.xml.parsers.DocumentBuilderFactory; 034import javax.xml.parsers.ParserConfigurationException; 035import javax.xml.transform.OutputKeys; 036import javax.xml.transform.Transformer; 037import javax.xml.transform.TransformerConfigurationException; 038import javax.xml.transform.TransformerException; 039import javax.xml.transform.TransformerFactory; 040import javax.xml.transform.TransformerFactoryConfigurationError; 041import javax.xml.transform.dom.DOMResult; 042import javax.xml.transform.dom.DOMSource; 043import javax.xml.transform.stream.StreamResult; 044import javax.xml.transform.stream.StreamSource; 045 046import com.google.common.collect.Maps; 047 048import org.slf4j.Logger; 049import org.slf4j.LoggerFactory; 050import org.w3c.dom.Document; 051import org.w3c.dom.Element; 052import org.w3c.dom.Node; 053import org.w3c.dom.NodeList; 054 055import org.apache.isis.applib.ViewModel; 056import org.apache.isis.applib.services.xmlsnapshot.XmlSnapshotService.Snapshot; 057import org.apache.isis.applib.snapshot.SnapshottableWithInclusions; 058import org.apache.isis.core.commons.exceptions.IsisException; 059import org.apache.isis.core.metamodel.adapter.ObjectAdapter; 060import org.apache.isis.core.metamodel.adapter.oid.OidMarshaller; 061import org.apache.isis.core.metamodel.facetapi.FacetUtil; 062import org.apache.isis.core.metamodel.facets.collections.modify.CollectionFacet; 063import org.apache.isis.core.metamodel.facets.object.encodeable.EncodableFacet; 064import org.apache.isis.core.metamodel.facets.object.parseable.ParseableFacet; 065import org.apache.isis.core.metamodel.facets.object.value.ValueFacet; 066import org.apache.isis.core.metamodel.spec.ObjectSpecification; 067import org.apache.isis.core.metamodel.spec.ObjectSpecificationException; 068import org.apache.isis.core.metamodel.spec.feature.Contributed; 069import org.apache.isis.core.metamodel.spec.feature.ObjectAssociation; 070import org.apache.isis.core.metamodel.spec.feature.OneToManyAssociation; 071import org.apache.isis.core.metamodel.spec.feature.OneToOneAssociation; 072 073/** 074 * Traverses object graph from specified root, so that an XML representation of 075 * the graph can be returned. 076 * 077 * <p> 078 * Initially designed to allow snapshots to be easily created. 079 * 080 * <p> 081 * Typical use: 082 * 083 * <pre> 084 * XmlSnapshot snapshot = new XmlSnapshot(customer); // where customer is a 085 * // reference to an 086 * // ObjectAdapter 087 * Element customerAsXml = snapshot.toXml(); // returns customer's fields, titles 088 * // of simple references, number of 089 * // items in collections 090 * snapshot.include("placeOfBirth"); // navigates to another object represented by 091 * // simple reference "placeOfBirth" 092 * snapshot.include("orders/product"); // navigates to all <tt>Order</tt>s of 093 * // <tt>Customer</tt>, and from them for 094 * // their <tt>Product</tt>s 095 * </pre> 096 */ 097public class XmlSnapshot implements Snapshot { 098 099 private static final Logger LOG = LoggerFactory.getLogger(XmlSnapshot.class); 100 101 private final IsisSchema isisMetaModel; 102 103 private final Place rootPlace; 104 105 private final XmlSchema schema; 106 107 /** 108 * the suggested location for the schema (xsi:schemaLocation attribute) 109 */ 110 private String schemaLocationFileName; 111 private boolean topLevelElementWritten = false; 112 113 private final Document xmlDocument; 114 115 /** 116 * root element of {@link #xmlDocument} 117 */ 118 private Element xmlElement; 119 private final Document xsdDocument; 120 /** 121 * root element of {@link #xsdDocument} 122 */ 123 private final Element xsdElement; 124 125 private final XsMetaModel xsMeta; 126 127 private final OidMarshaller oidMarshaller; 128 129 /** 130 * Start a snapshot at the root object, using own namespace manager. 131 * 132 * @param oidMarshaller 133 * TODO 134 */ 135 public XmlSnapshot(final ObjectAdapter rootAdapter, OidMarshaller oidMarshaller) { 136 this(rootAdapter, new XmlSchema(), oidMarshaller); 137 } 138 139 /** 140 * Start a snapshot at the root object, using supplied namespace manager. 141 * 142 * @param oidMarshaller 143 * TODO 144 */ 145 public XmlSnapshot(final ObjectAdapter rootAdapter, final XmlSchema schema, final OidMarshaller oidMarshaller) { 146 147 if (LOG.isDebugEnabled()) { 148 LOG.debug(".ctor(" + log("rootObj", rootAdapter) + andlog("schema", schema) + andlog("addOids", "" + true) + ")"); 149 } 150 151 this.isisMetaModel = new IsisSchema(); 152 this.xsMeta = new XsMetaModel(); 153 154 this.schema = schema; 155 this.oidMarshaller = oidMarshaller; 156 157 final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); 158 dbf.setNamespaceAware(true); 159 DocumentBuilder db; 160 try { 161 db = dbf.newDocumentBuilder(); 162 this.xmlDocument = db.newDocument(); 163 this.xsdDocument = db.newDocument(); 164 165 xsdElement = xsMeta.createXsSchemaElement(xsdDocument); 166 167 this.rootPlace = appendXml(rootAdapter); 168 169 } catch (final ParserConfigurationException e) { 170 LOG.error("unable to build snapshot", e); 171 throw new IsisException(e); 172 } 173 174 for (final String path : getPathsFor(rootAdapter.getObject())) { 175 include(path); 176 } 177 178 } 179 180 private List<String> getPathsFor(final Object object) { 181 if (!(object instanceof SnapshottableWithInclusions)) { 182 return Collections.emptyList(); 183 } 184 final List<String> paths = ((SnapshottableWithInclusions) object).snapshotInclusions(); 185 if (paths == null) { 186 return Collections.emptyList(); 187 } 188 return paths; 189 } 190 191 private String andlog(final String label, final ObjectAdapter object) { 192 return ", " + log(label, object); 193 } 194 195 private String andlog(final String label, final Object object) { 196 return ", " + log(label, object); 197 } 198 199 /** 200 * Creates an Element representing this object, and appends it as the root 201 * element of the Document. 202 * 203 * The Document must not yet have a root element Additionally, the supplied 204 * schemaManager must be populated with any application-level namespaces 205 * referenced in the document that the parentElement resides within. 206 * (Normally this is achieved simply by using appendXml passing in a new 207 * schemaManager - see {@link #toXml()}or {@link XmlSnapshot}). 208 */ 209 private Place appendXml(final ObjectAdapter object) { 210 211 if (LOG.isDebugEnabled()) { 212 LOG.debug("appendXml(" + log("obj", object) + "')"); 213 } 214 215 final String fullyQualifiedClassName = object.getSpecification().getFullIdentifier(); 216 217 schema.setUri(fullyQualifiedClassName); // derive 218 // URI 219 // from 220 // fully 221 // qualified 222 // name 223 224 final Place place = objectToElement(object); 225 226 final Element element = place.getXmlElement(); 227 final Element xsElementElement = place.getXsdElement(); 228 229 if (LOG.isDebugEnabled()) { 230 LOG.debug("appendXml(NO): add as element to XML doc"); 231 } 232 getXmlDocument().appendChild(element); 233 234 if (LOG.isDebugEnabled()) { 235 LOG.debug("appendXml(NO): add as xs:element to xs:schema of the XSD document"); 236 } 237 getXsdElement().appendChild(xsElementElement); 238 239 if (LOG.isDebugEnabled()) { 240 LOG.debug("appendXml(NO): set target name in XSD, derived from FQCN of obj"); 241 } 242 schema.setTargetNamespace(getXsdDocument(), fullyQualifiedClassName); 243 244 if (LOG.isDebugEnabled()) { 245 LOG.debug("appendXml(NO): set schema location file name to XSD, derived from FQCN of obj"); 246 } 247 final String schemaLocationFileName = fullyQualifiedClassName + ".xsd"; 248 schema.assignSchema(getXmlDocument(), fullyQualifiedClassName, schemaLocationFileName); 249 250 if (LOG.isDebugEnabled()) { 251 LOG.debug("appendXml(NO): copy into snapshot obj"); 252 } 253 setXmlElement(element); 254 setSchemaLocationFileName(schemaLocationFileName); 255 256 return place; 257 } 258 259 /** 260 * Creates an Element representing this object, and appends it to the 261 * supplied parentElement, provided that an element for the object is not 262 * already appended. 263 * 264 * The method uses the OID to determine if an object's element is already 265 * present. If the object is not yet persistent, then the hashCode is used 266 * instead. 267 * 268 * The parentElement must have an owner document, and should define the "isis" 269 * namespace. Additionally, the supplied schemaManager must be populated 270 * with any application-level namespaces referenced in the document that the 271 * parentElement resides within. (Normally this is achieved simply by using 272 * appendXml passing in a rootElement and a new schemaManager - see 273 * {@link #toXml()}or {@link XmlSnapshot}). 274 */ 275 private Element appendXml(final Place parentPlace, final ObjectAdapter childObject) { 276 277 if (LOG.isDebugEnabled()) { 278 LOG.debug("appendXml(" + log("parentPlace", parentPlace) + andlog("childObj", childObject) + ")"); 279 } 280 281 final Element parentElement = parentPlace.getXmlElement(); 282 final Element parentXsElement = parentPlace.getXsdElement(); 283 284 if (parentElement.getOwnerDocument() != getXmlDocument()) { 285 throw new IllegalArgumentException("parent XML Element must have snapshot's XML document as its owner"); 286 } 287 288 if (LOG.isDebugEnabled()) { 289 LOG.debug("appendXml(Pl, NO): invoking objectToElement() for " + log("childObj", childObject)); 290 } 291 final Place childPlace = objectToElement(childObject); 292 Element childElement = childPlace.getXmlElement(); 293 final Element childXsElement = childPlace.getXsdElement(); 294 295 if (LOG.isDebugEnabled()) { 296 LOG.debug("appendXml(Pl, NO): invoking mergeTree of parent with child"); 297 } 298 childElement = mergeTree(parentElement, childElement); 299 300 if (LOG.isDebugEnabled()) { 301 LOG.debug("appendXml(Pl, NO): adding XS Element to schema if required"); 302 } 303 schema.addXsElementIfNotPresent(parentXsElement, childXsElement); 304 305 return childElement; 306 } 307 308 private boolean appendXmlThenIncludeRemaining(final Place parentPlace, final ObjectAdapter referencedObject, final Vector fieldNames, final String annotation) { 309 310 if (LOG.isDebugEnabled()) { 311 LOG.debug("appendXmlThenIncludeRemaining(: " + log("parentPlace", parentPlace) + andlog("referencedObj", referencedObject) + andlog("fieldNames", fieldNames) + andlog("annotation", annotation) + ")"); 312 LOG.debug("appendXmlThenIncludeRemaining(..): invoking appendXml(parentPlace, referencedObject)"); 313 } 314 315 final Element referencedElement = appendXml(parentPlace, referencedObject); 316 final Place referencedPlace = new Place(referencedObject, referencedElement); 317 318 final boolean includedField = includeField(referencedPlace, fieldNames, annotation); 319 320 if (LOG.isDebugEnabled()) { 321 LOG.debug("appendXmlThenIncludeRemaining(..): invoked includeField(referencedPlace, fieldNames)" + andlog("returned", "" + includedField)); 322 } 323 324 return includedField; 325 } 326 327 private Vector elementsUnder(final Element parentElement, final String localName) { 328 final Vector v = new Vector(); 329 final NodeList existingNodes = parentElement.getChildNodes(); 330 for (int i = 0; i < existingNodes.getLength(); i++) { 331 final Node node = existingNodes.item(i); 332 if (!(node instanceof Element)) { 333 continue; 334 } 335 final Element element = (Element) node; 336 if (localName.equals("*") || element.getLocalName().equals(localName)) { 337 v.addElement(element); 338 } 339 } 340 return v; 341 } 342 343 public ObjectAdapter getObject() { 344 return rootPlace.getObject(); 345 } 346 347 public XmlSchema getSchema() { 348 return schema; 349 } 350 351 /** 352 * The name of the <code>xsi:schemaLocation</code> in the XML document. 353 * 354 * Taken from the <code>fullyQualifiedClassName</code> (which also is used 355 * as the basis for the <code>targetNamespace</code>. 356 * 357 * Populated in {@link #appendXml(ObjectAdapter)}. 358 */ 359 public String getSchemaLocationFileName() { 360 return schemaLocationFileName; 361 } 362 363 public Document getXmlDocument() { 364 return xmlDocument; 365 } 366 367 /** 368 * The root element of {@link #getXmlDocument()}. Returns <code>null</code> 369 * until the snapshot has actually been built. 370 */ 371 public Element getXmlElement() { 372 return xmlElement; 373 } 374 375 public Document getXsdDocument() { 376 return xsdDocument; 377 } 378 379 /** 380 * The root element of {@link #getXsdDocument()}. Returns <code>null</code> 381 * until the snapshot has actually been built. 382 */ 383 public Element getXsdElement() { 384 return xsdElement; 385 } 386 387 public void include(final String path) { 388 include(path, null); 389 } 390 391 public void include(final String path, final String annotation) { 392 393 // tokenize into successive fields 394 final Vector fieldNames = new Vector(); 395 for (final StringTokenizer tok = new StringTokenizer(path, "/"); tok.hasMoreTokens();) { 396 final String token = tok.nextToken(); 397 398 if (LOG.isDebugEnabled()) { 399 LOG.debug("include(..): " + log("token", token)); 400 } 401 fieldNames.addElement(token); 402 } 403 404 if (LOG.isDebugEnabled()) { 405 LOG.debug("include(..): " + log("fieldNames", fieldNames)); 406 } 407 408 // navigate first field, from the root. 409 if (LOG.isDebugEnabled()) { 410 LOG.debug("include(..): invoking includeField"); 411 } 412 includeField(rootPlace, fieldNames, annotation); 413 } 414 415 /** 416 * @return true if able to navigate the complete vector of field names 417 * successfully; false if a field could not be located or it turned 418 * out to be a value. 419 */ 420 private boolean includeField(final Place place, final Vector fieldNames, final String annotation) { 421 422 if (LOG.isDebugEnabled()) { 423 LOG.debug("includeField(: " + log("place", place) + andlog("fieldNames", fieldNames) + andlog("annotation", annotation) + ")"); 424 } 425 426 final ObjectAdapter object = place.getObject(); 427 final Element xmlElement = place.getXmlElement(); 428 429 // we use a copy of the path so that we can safely traverse collections 430 // without side-effects 431 final Vector originalNames = fieldNames; 432 final Vector names = new Vector(); 433 for (final java.util.Enumeration e = originalNames.elements(); e.hasMoreElements();) { 434 names.addElement(e.nextElement()); 435 } 436 437 // see if we have any fields to process 438 if (names.size() == 0) { 439 return true; 440 } 441 442 // take the first field name from the list, and remove 443 final String fieldName = (String) names.elementAt(0); 444 names.removeElementAt(0); 445 446 if (LOG.isDebugEnabled()) { 447 LOG.debug("includeField(Pl, Vec, Str):" + log("processing field", fieldName) + andlog("left", "" + names.size())); 448 } 449 450 // locate the field in the object's class 451 final ObjectSpecification nos = object.getSpecification(); 452 ObjectAssociation field = null; 453 try { 454 // HACK: really want a ObjectSpecification.hasField method to 455 // check first. 456 field = nos.getAssociation(fieldName); 457 } catch (final ObjectSpecificationException ex) { 458 if (LOG.isInfoEnabled()) { 459 LOG.info("includeField(Pl, Vec, Str): could not locate field, skipping"); 460 } 461 return false; 462 } 463 464 // locate the corresponding XML element 465 // (the corresponding XSD element will later be attached to xmlElement 466 // as its userData) 467 if (LOG.isDebugEnabled()) { 468 LOG.debug("includeField(Pl, Vec, Str): locating corresponding XML element"); 469 } 470 final Vector xmlFieldElements = elementsUnder(xmlElement, field.getId()); 471 if (xmlFieldElements.size() != 1) { 472 if (LOG.isInfoEnabled()) { 473 LOG.info("includeField(Pl, Vec, Str): could not locate " + log("field", field.getId()) + andlog("xmlFieldElements.size", "" + xmlFieldElements.size())); 474 } 475 return false; 476 } 477 final Element xmlFieldElement = (Element) xmlFieldElements.elementAt(0); 478 479 if (names.size() == 0 && annotation != null) { 480 // nothing left in the path, so we will apply the annotation now 481 isisMetaModel.setAnnotationAttribute(xmlFieldElement, annotation); 482 } 483 484 final Place fieldPlace = new Place(object, xmlFieldElement); 485 486 if (field instanceof OneToOneAssociation) { 487 if (field.getSpecification().getAssociations(Contributed.EXCLUDED).size() == 0) { 488 if (LOG.isDebugEnabled()) { 489 LOG.debug("includeField(Pl, Vec, Str): field is value; done"); 490 } 491 return false; 492 } 493 494 if (LOG.isDebugEnabled()) { 495 LOG.debug("includeField(Pl, Vec, Str): field is 1->1"); 496 } 497 498 final OneToOneAssociation oneToOneAssociation = ((OneToOneAssociation) field); 499 final ObjectAdapter referencedObject = oneToOneAssociation.get(fieldPlace.getObject()); 500 501 if (referencedObject == null) { 502 return true; // not a failure if the reference was null 503 } 504 505 final boolean appendedXml = appendXmlThenIncludeRemaining(fieldPlace, referencedObject, names, annotation); 506 if (LOG.isDebugEnabled()) { 507 LOG.debug("includeField(Pl, Vec, Str): 1->1: invoked appendXmlThenIncludeRemaining for " + log("referencedObj", referencedObject) + andlog("returned", "" + appendedXml)); 508 } 509 510 return appendedXml; 511 512 } else if (field instanceof OneToManyAssociation) { 513 if (LOG.isDebugEnabled()) { 514 LOG.debug("includeField(Pl, Vec, Str): field is 1->M"); 515 } 516 517 final OneToManyAssociation oneToManyAssociation = (OneToManyAssociation) field; 518 final ObjectAdapter collection = oneToManyAssociation.get(fieldPlace.getObject()); 519 final CollectionFacet facet = collection.getSpecification().getFacet(CollectionFacet.class); 520 521 if (LOG.isDebugEnabled()) { 522 LOG.debug("includeField(Pl, Vec, Str): 1->M: " + log("collection.size", "" + facet.size(collection))); 523 } 524 boolean allFieldsNavigated = true; 525 for (final ObjectAdapter referencedObject : facet.iterable(collection)) { 526 final boolean appendedXml = appendXmlThenIncludeRemaining(fieldPlace, referencedObject, names, annotation); 527 if (LOG.isDebugEnabled()) { 528 LOG.debug("includeField(Pl, Vec, Str): 1->M: + invoked appendXmlThenIncludeRemaining for " + log("referencedObj", referencedObject) + andlog("returned", "" + appendedXml)); 529 } 530 allFieldsNavigated = allFieldsNavigated && appendedXml; 531 } 532 LOG.debug("includeField(Pl, Vec, Str): " + log("returning", "" + allFieldsNavigated)); 533 return allFieldsNavigated; 534 } 535 536 return false; // fall through, shouldn't get here but just in 537 // case. 538 } 539 540 private String log(final String label, final ObjectAdapter adapter) { 541 return log(label, (adapter == null ? "(null)" : adapter.titleString() + "[" + oidAsString(adapter) + "]")); 542 } 543 544 private String log(final String label, final Object pojo) { 545 return (label == null ? "?" : label) + "='" + (pojo == null ? "(null)" : pojo.toString()) + "'"; 546 } 547 548 /** 549 * Merges the tree of Elements whose root is <code>childElement</code> 550 * underneath the <code>parentElement</code>. 551 * 552 * If the <code>parentElement</code> already has an element that matches the 553 * <code>childElement</code>, then recursively attaches the grandchildren 554 * instead. 555 * 556 * The element returned will be either the supplied 557 * <code>childElement</code>, or an existing child element if one already 558 * existed under <code>parentElement</code>. 559 */ 560 private Element mergeTree(final Element parentElement, final Element childElement) { 561 562 if (LOG.isDebugEnabled()) { 563 LOG.debug("mergeTree(" + log("parent", parentElement) + andlog("child", childElement)); 564 } 565 566 final String childElementOid = isisMetaModel.getAttribute(childElement, "oid"); 567 568 if (LOG.isDebugEnabled()) { 569 LOG.debug("mergeTree(El,El): " + log("childOid", childElementOid)); 570 } 571 if (childElementOid != null) { 572 573 // before we add the child element, check to see if it is already 574 // there 575 if (LOG.isDebugEnabled()) { 576 LOG.debug("mergeTree(El,El): check if child already there"); 577 } 578 final Vector existingChildElements = elementsUnder(parentElement, childElement.getLocalName()); 579 for (final Enumeration childEnum = existingChildElements.elements(); childEnum.hasMoreElements();) { 580 final Element possibleMatchingElement = (Element) childEnum.nextElement(); 581 582 final String possibleMatchOid = isisMetaModel.getAttribute(possibleMatchingElement, "oid"); 583 if (possibleMatchOid == null || !possibleMatchOid.equals(childElementOid)) { 584 continue; 585 } 586 587 if (LOG.isDebugEnabled()) { 588 LOG.debug("mergeTree(El,El): child already there; merging grandchildren"); 589 } 590 591 // match: transfer the children of the child (grandchildren) to 592 // the 593 // already existing matching child 594 final Element existingChildElement = possibleMatchingElement; 595 final Vector grandchildrenElements = elementsUnder(childElement, "*"); 596 for (final Enumeration grandchildEnum = grandchildrenElements.elements(); grandchildEnum.hasMoreElements();) { 597 final Element grandchildElement = (Element) grandchildEnum.nextElement(); 598 childElement.removeChild(grandchildElement); 599 600 if (LOG.isDebugEnabled()) { 601 LOG.debug("mergeTree(El,El): merging " + log("grandchild", grandchildElement)); 602 } 603 604 mergeTree(existingChildElement, grandchildElement); 605 } 606 return existingChildElement; 607 } 608 } 609 610 parentElement.appendChild(childElement); 611 return childElement; 612 } 613 614 Place objectToElement(final ObjectAdapter adapter) { 615 616 if (LOG.isDebugEnabled()) { 617 LOG.debug("objectToElement(" + log("object", adapter) + ")"); 618 } 619 620 final ObjectSpecification nos = adapter.getSpecification(); 621 622 if (LOG.isDebugEnabled()) { 623 LOG.debug("objectToElement(NO): create element and isis:title"); 624 } 625 final Element element = schema.createElement(getXmlDocument(), nos.getShortIdentifier(), nos.getFullIdentifier(), nos.getSingularName(), nos.getPluralName()); 626 isisMetaModel.appendIsisTitle(element, adapter.titleString()); 627 628 if (LOG.isDebugEnabled()) { 629 LOG.debug("objectToElement(NO): create XS element for Isis class"); 630 } 631 final Element xsElement = schema.createXsElementForNofClass(getXsdDocument(), element, topLevelElementWritten, FacetUtil.getFacetsByType(nos)); 632 633 // hack: every element in the XSD schema apart from first needs minimum 634 // cardinality setting. 635 topLevelElementWritten = true; 636 637 final Place place = new Place(adapter, element); 638 639 isisMetaModel.setAttributesForClass(element, oidAsString(adapter).toString()); 640 641 final List<ObjectAssociation> fields = nos.getAssociations(Contributed.EXCLUDED); 642 if (LOG.isDebugEnabled()) { 643 LOG.debug("objectToElement(NO): processing fields"); 644 } 645 eachField: for (int i = 0; i < fields.size(); i++) { 646 final ObjectAssociation field = fields.get(i); 647 final String fieldName = field.getId(); 648 649 if (LOG.isDebugEnabled()) { 650 LOG.debug("objectToElement(NO): " + log("field", fieldName)); 651 } 652 653 // Skip field if we have seen the name already 654 // This is a workaround for getLastActivity(). This method exists 655 // in AbstractObjectAdapter, but is not (at some level) being picked 656 // up 657 // by the dot-net reflector as a property. On the other hand it does 658 // exist as a field in the meta model (ObjectSpecification). 659 // 660 // Now, to re-expose the lastactivity field for .Net, a 661 // deriveLastActivity() 662 // has been added to BusinessObject. This caused another field of 663 // the 664 // same name, ultimately breaking the XSD. 665 for (int j = 0; j < i; j++) { 666 if (fieldName.equals(fields.get(i).getName())) { 667 LOG.debug("objectToElement(NO): " + log("field", fieldName) + " SKIPPED"); 668 continue eachField; 669 } 670 } 671 672 Element xmlFieldElement = getXmlDocument().createElementNS(schema.getUri(), // scoped 673 // by 674 // namespace 675 // of class of 676 // containing object 677 schema.getPrefix() + ":" + fieldName); 678 679 Element xsdFieldElement = null; 680 681 if (field.getSpecification().containsFacet(ValueFacet.class)) { 682 if (LOG.isDebugEnabled()) { 683 LOG.debug("objectToElement(NO): " + log("field", fieldName) + " is value"); 684 } 685 686 final ObjectSpecification fieldNos = field.getSpecification(); 687 // skip fields of type XmlValue 688 if (fieldNos == null) { 689 continue eachField; 690 } 691 if (fieldNos.getFullIdentifier() != null && fieldNos.getFullIdentifier().endsWith("XmlValue")) { 692 continue eachField; 693 } 694 695 final OneToOneAssociation valueAssociation = ((OneToOneAssociation) field); 696 final Element xmlValueElement = xmlFieldElement; // more 697 // meaningful 698 // locally 699 // scoped name 700 701 ObjectAdapter value; 702 try { 703 value = valueAssociation.get(adapter); 704 705 final ObjectSpecification valueNos = value.getSpecification(); 706 707 // XML 708 isisMetaModel.setAttributesForValue(xmlValueElement, valueNos.getShortIdentifier()); 709 710 // return parsed string, else encoded string, else title. 711 String valueStr; 712 final ParseableFacet parseableFacet = fieldNos.getFacet(ParseableFacet.class); 713 final EncodableFacet encodeableFacet = fieldNos.getFacet(EncodableFacet.class); 714 if (parseableFacet != null) { 715 valueStr = parseableFacet.parseableTitle(value); 716 } else if (encodeableFacet != null) { 717 valueStr = encodeableFacet.toEncodedString(value); 718 } else { 719 valueStr = value.titleString(); 720 } 721 722 final boolean notEmpty = (valueStr.length() > 0); 723 if (notEmpty) { 724 xmlValueElement.appendChild(getXmlDocument().createTextNode(valueStr)); 725 } else { 726 isisMetaModel.setIsEmptyAttribute(xmlValueElement, true); 727 } 728 729 } catch (final Exception ex) { 730 LOG.warn("objectToElement(NO): " + log("field", fieldName) + ": getField() threw exception - skipping XML generation"); 731 } 732 733 // XSD 734 xsdFieldElement = schema.createXsElementForNofValue(xsElement, xmlValueElement, FacetUtil.getFacetsByType(valueAssociation)); 735 736 } else if (field instanceof OneToOneAssociation) { 737 738 if (LOG.isDebugEnabled()) { 739 LOG.debug("objectToElement(NO): " + log("field", fieldName) + " is OneToOneAssociation"); 740 } 741 742 final OneToOneAssociation oneToOneAssociation = ((OneToOneAssociation) field); 743 final String fullyQualifiedClassName = nos.getFullIdentifier(); 744 final Element xmlReferenceElement = xmlFieldElement; // more 745 // meaningful 746 // locally 747 // scoped 748 // name 749 750 ObjectAdapter referencedObjectAdapter; 751 752 try { 753 referencedObjectAdapter = oneToOneAssociation.get(adapter); 754 755 // XML 756 isisMetaModel.setAttributesForReference(xmlReferenceElement, schema.getPrefix(), fullyQualifiedClassName); 757 758 if (referencedObjectAdapter != null) { 759 isisMetaModel.appendIsisTitle(xmlReferenceElement, referencedObjectAdapter.titleString()); 760 } else { 761 isisMetaModel.setIsEmptyAttribute(xmlReferenceElement, true); 762 } 763 764 } catch (final Exception ex) { 765 LOG.warn("objectToElement(NO): " + log("field", fieldName) + ": getAssociation() threw exception - skipping XML generation"); 766 } 767 768 // XSD 769 xsdFieldElement = schema.createXsElementForNofReference(xsElement, xmlReferenceElement, oneToOneAssociation.getSpecification().getFullIdentifier(), FacetUtil.getFacetsByType(oneToOneAssociation)); 770 771 } else if (field instanceof OneToManyAssociation) { 772 773 if (LOG.isDebugEnabled()) { 774 LOG.debug("objectToElement(NO): " + log("field", fieldName) + " is OneToManyAssociation"); 775 } 776 777 final OneToManyAssociation oneToManyAssociation = (OneToManyAssociation) field; 778 final Element xmlCollectionElement = xmlFieldElement; // more 779 // meaningful 780 // locally 781 // scoped 782 // name 783 784 ObjectAdapter collection; 785 try { 786 collection = oneToManyAssociation.get(adapter); 787 final ObjectSpecification referencedTypeNos = oneToManyAssociation.getSpecification(); 788 final String fullyQualifiedClassName = referencedTypeNos.getFullIdentifier(); 789 790 // XML 791 isisMetaModel.setIsisCollection(xmlCollectionElement, schema.getPrefix(), fullyQualifiedClassName, collection); 792 } catch (final Exception ex) { 793 LOG.warn("objectToElement(NO): " + log("field", fieldName) + ": get(obj) threw exception - skipping XML generation"); 794 } 795 796 // XSD 797 xsdFieldElement = schema.createXsElementForNofCollection(xsElement, xmlCollectionElement, oneToManyAssociation.getSpecification().getFullIdentifier(), FacetUtil.getFacetsByType(oneToManyAssociation)); 798 799 } else { 800 if (LOG.isInfoEnabled()) { 801 LOG.info("objectToElement(NO): " + log("field", fieldName) + " is unknown type; ignored"); 802 } 803 continue; 804 } 805 806 if (xsdFieldElement != null) { 807 Place.setXsdElement(xmlFieldElement, xsdFieldElement); 808 } 809 810 // XML 811 if (LOG.isDebugEnabled()) { 812 LOG.debug("objectToElement(NO): invoking mergeTree for field"); 813 } 814 xmlFieldElement = mergeTree(element, xmlFieldElement); 815 816 // XSD 817 if (xsdFieldElement != null) { 818 if (LOG.isDebugEnabled()) { 819 LOG.debug("objectToElement(NO): adding XS element for field to schema"); 820 } 821 schema.addFieldXsElement(xsElement, xsdFieldElement); 822 } 823 } 824 825 return place; 826 } 827 828 829 private final Map<ObjectAdapter, String> viewModelFakeOids = Maps.newHashMap(); 830 831 private String oidAsString(final ObjectAdapter adapter) { 832 if(adapter.getObject() instanceof ViewModel) { 833 // return a fake oid for view models; 834 // a snapshot may be being used to create the memento/OID 835 String fakeOid = viewModelFakeOids.get(adapter); 836 if(fakeOid == null) { 837 fakeOid = "viewmodel-fakeoid-" + UUID.randomUUID().toString(); 838 viewModelFakeOids.put(adapter, fakeOid); 839 } 840 return fakeOid; 841 } else { 842 return adapter.getOid().enString(oidMarshaller); 843 } 844 } 845 846 /** 847 * @param schemaLocationFileName 848 * The schemaLocationFileName to set. 849 */ 850 private void setSchemaLocationFileName(final String schemaLocationFileName) { 851 this.schemaLocationFileName = schemaLocationFileName; 852 } 853 854 /** 855 * @param xmlElement 856 * The xmlElement to set. 857 */ 858 private void setXmlElement(final Element xmlElement) { 859 this.xmlElement = xmlElement; 860 } 861 862 @Override 863 public String getXmlDocumentAsString() { 864 final Document doc = getXmlDocument(); 865 return asString(doc); 866 } 867 868 @Override 869 public String getXsdDocumentAsString() { 870 final Document doc = getXsdDocument(); 871 return asString(doc); 872 } 873 874 private static String asString(final Document doc) { 875 try { 876 final DOMSource domSource = new DOMSource(doc); 877 final StringWriter writer = new StringWriter(); 878 final StreamResult result = new StreamResult(writer); 879 final TransformerFactory tf = TransformerFactory.newInstance(); 880 final Transformer transformer = tf.newTransformer(); 881 transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); 882 transformer.setOutputProperty(OutputKeys.METHOD, "xml"); 883 transformer.setOutputProperty(OutputKeys.INDENT, "yes"); 884 transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); 885 transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); 886 transformer.transform(domSource, result); 887 888 return writer.toString(); 889 } catch (TransformerConfigurationException e) { 890 throw new IsisException(e); 891 } catch (TransformerException e) { 892 throw new IsisException(e); 893 } 894 } 895 896 897}