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(&quot;placeOfBirth&quot;); // navigates to another object represented by
091 * // simple reference &quot;placeOfBirth&quot;
092 * snapshot.include(&quot;orders/product&quot;); // navigates to all &lt;tt&gt;Order&lt;/tt&gt;s of
093 * // &lt;tt&gt;Customer&lt;/tt&gt;, and from them for
094 * // their &lt;tt&gt;Product&lt;/tt&gt;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 &quot;isis&quot;
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}