/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.isis.core.runtime.snapshot; import java.util.Enumeration; import java.util.Hashtable; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.NodeList; import org.apache.isis.applib.services.xmlsnapshot.XmlSnapshotService.Snapshot; /** * Represents the schema for the derived snapshot. */ public final class XmlSchema { private final String prefix; private final String uriBase; private String uri; private final IsisSchema isisMeta; private final XsMetaModel xsMeta; private final Helper helper; /** * The base part of the namespace prefix to use if none explicitly supplied * in the constructor. */ public final static String DEFAULT_PREFIX = "app"; public XmlSchema() { this(IsisSchema.DEFAULT_URI_BASE, XmlSchema.DEFAULT_PREFIX); } /** * @param uriBase * the prefix for the application namespace's URIs * @param prefix * the prefix for the application namespace's prefix */ public XmlSchema(final String uriBase, final String prefix) { this.isisMeta = new IsisSchema(); this.xsMeta = new XsMetaModel(); this.helper = new Helper(); final String base = new Helper().trailingSlash(uriBase); if (XsMetaModel.W3_ORG_XMLNS_URI.equals(base)) { throw new IllegalArgumentException("Namespace URI reserved for w3.org XMLNS namespace"); } if (XsMetaModel.W3_ORG_XMLNS_PREFIX.equals(prefix)) { throw new IllegalArgumentException("Namespace prefix reserved for w3.org XMLNS namespace."); } if (XsMetaModel.W3_ORG_XS_URI.equals(base)) { throw new IllegalArgumentException("Namespace URI reserved for w3.org XML schema namespace."); } if (XsMetaModel.W3_ORG_XS_PREFIX.equals(prefix)) { throw new IllegalArgumentException("Namespace prefix reserved for w3.org XML schema namespace."); } if (XsMetaModel.W3_ORG_XSI_URI.equals(base)) { throw new IllegalArgumentException("Namespace URI reserved for w3.org XML schema-instance namespace."); } if (XsMetaModel.W3_ORG_XSI_PREFIX.equals(prefix)) { throw new IllegalArgumentException("Namespace prefix reserved for w3.org XML schema-instance namespace."); } if (IsisSchema.NS_URI.equals(base)) { throw new IllegalArgumentException("Namespace URI reserved for NOF metamodel namespace."); } if (IsisSchema.NS_PREFIX.equals(prefix)) { throw new IllegalArgumentException("Namespace prefix reserved for NOF metamodel namespace."); } this.uriBase = base; this.prefix = prefix; } /** * The base of the Uri in use. All namespaces are concatenated with this. * * The namespace string will be the concatenation of the plus the package * name of the class of the object being referenced. * * If not specified in the constructor, then {@link #DEFAULT_URI_PREFIX} is * used. */ public String getUriBase() { return uriBase; } /** * Returns the namespace URI for the class. */ void setUri(final String fullyQualifiedClassName) { if (uri != null) { throw new IllegalStateException("URI has already been specified."); } this.uri = getUriBase() + helper.packageNameFor(fullyQualifiedClassName) + "/" + helper.classNameFor(fullyQualifiedClassName); } /** * The URI of the application namespace. * * The value returned will be <code>null</code> until a {@link Snapshot} is * created. */ public String getUri() { if (uri == null) { throw new IllegalStateException("URI has not been specified."); } return uri; } /** * The prefix to the namespace for the application. */ public String getPrefix() { return this.prefix; } /** * Creates an element with the specified localName, in the appropriate * namespace for the NOS. * * If necessary the namespace definition is added to the root element of the * doc used to create the element. The element is not parented but to avoid * an error can only be added as a child of another element in the same doc. */ Element createElement(final Document doc, final String localName, final String fullyQualifiedClassName, final String singularName, final String pluralName) { final Element element = doc.createElementNS(getUri(), getPrefix() + ":" + localName); element.setAttributeNS(IsisSchema.NS_URI, IsisSchema.NS_PREFIX + ":fqn", fullyQualifiedClassName); element.setAttributeNS(IsisSchema.NS_URI, IsisSchema.NS_PREFIX + ":singular", singularName); element.setAttributeNS(IsisSchema.NS_URI, IsisSchema.NS_PREFIX + ":plural", pluralName); isisMeta.addNamespace(element); // good a place as any addNamespace(element, getPrefix(), getUri()); return element; } /** * Sets the target namespace for the XSD document to a URI derived from the * fully qualified class name of the supplied object */ void setTargetNamespace(final Document xsdDoc, final String fullyQualifiedClassName) { final Element xsSchemaElement = xsdDoc.getDocumentElement(); if (xsSchemaElement == null) { throw new IllegalArgumentException("XSD Document must have <xs:schema> element attached"); } // targetNamespace="http://isis.apache.org/ns/app/<fully qualified class // name> xsSchemaElement.setAttribute("targetNamespace", getUri()); addNamespace(xsSchemaElement, getPrefix(), getUri()); } /** * Creates an <xs:element> element defining the presence of the named * element representing a class */ Element createXsElementForNofClass(final Document xsdDoc, final Element element, final boolean addCardinality, final Hashtable extensions) { // gather details from XML element final String localName = element.getLocalName(); // <xs:element name="AO11ConfirmAnimalRegistration"> // <xs:complexType> // <xs:sequence> // <xs:element ref="isis:title"/> // <!-- placeholder --> // </xs:sequence> // <xs:attribute ref="isis:feature" // default="class"/> // <xs:attribute ref="isis:oid"/> // <xs:attribute ref="isis:annotation"/> // <xs:attribute ref="isis:fqn"/> // </xs:complexType> // </xs:element> // xs:element/@name="class name" // add to XML schema as a global attribute final Element xsElementForNofClassElement = xsMeta.createXsElementElement(xsdDoc, localName, addCardinality); // xs:element/xs:complexType // xs:element/xs:complexType/xs:sequence final Element xsComplexTypeElement = xsMeta.complexTypeFor(xsElementForNofClassElement); final Element xsSequenceElement = xsMeta.sequenceFor(xsComplexTypeElement); // xs:element/xs:complexType/xs:sequence/xs:element ref="isis:title" final Element xsTitleElement = xsMeta.createXsElement(helper.docFor(xsSequenceElement), "element"); xsTitleElement.setAttribute("ref", IsisSchema.NS_PREFIX + ":" + "title"); xsSequenceElement.appendChild(xsTitleElement); xsMeta.setXsCardinality(xsTitleElement, 0, 1); // xs:element/xs:complexType/xs:sequence/xs:element ref="extensions" addXsElementForAppExtensions(xsSequenceElement, extensions); // xs:element/xs:complexType/xs:attribute ... xsMeta.addXsIsisFeatureAttributeElements(xsComplexTypeElement, "class"); xsMeta.addXsIsisAttribute(xsComplexTypeElement, "oid"); xsMeta.addXsIsisAttribute(xsComplexTypeElement, "fqn"); xsMeta.addXsIsisAttribute(xsComplexTypeElement, "singular"); xsMeta.addXsIsisAttribute(xsComplexTypeElement, "plural"); xsMeta.addXsIsisAttribute(xsComplexTypeElement, "annotation"); Place.setXsdElement(element, xsElementForNofClassElement); return xsElementForNofClassElement; } /** * Creates an <code>xs:element</code> element to represent a collection of * application-defined extensions * * The returned element should be appended to <code>xs:sequence</code> * element of the xs:element representing the type of the owning object. */ void addXsElementForAppExtensions(final Element parentXsElementElement, final Hashtable extensions) { if (extensions.size() == 0) { return; } // <xs:element name="extensions"> // <xs:complexType> // <xs:sequence> // <xs:element name="app:%extension class short name%" minOccurs="0" // maxOccurs="1" default="%value%"/> // <xs:element name="app:%extension class short name%" minOccurs="0" // maxOccurs="1" default="%value%"/> // ... // <xs:element name="app:%extension class short name%" minOccurs="0" // maxOccurs="1" default="%value%"/> // </xs:sequence> // </xs:complexType> // </xs:element> // xs:element name="isis-extensions" // xs:element/xs:complexType/xs:sequence final Element xsExtensionsSequenceElement = addExtensionsElement(parentXsElementElement); addExtensionElements(xsExtensionsSequenceElement, extensions); return; } /** * Adds an isis-extensions element and a complexType and sequence elements * underneath. * * <p> * Returns the sequence element so that it can be appended to. */ private Element addExtensionsElement(final Element parentXsElement) { final Element xsExtensionsElementElement = xsMeta.createXsElementElement(helper.docFor(parentXsElement), "isis-extensions"); parentXsElement.appendChild(xsExtensionsElementElement); // xs:element/xs:complexType/xs:sequence/xs:element/xs:complexType/xs:sequence final Element xsExtensionsComplexTypeElement = xsMeta.complexTypeFor(xsExtensionsElementElement); final Element xsExtensionsSequenceElement = xsMeta.sequenceFor(xsExtensionsComplexTypeElement); return xsExtensionsSequenceElement; } private String shortName(final String className) { final int lastPeriodIdx = className.lastIndexOf('.'); if (lastPeriodIdx < 0) { return className; } return className.substring(lastPeriodIdx + 1); } /** * Creates an <code>xs:element</code> element to represent a value field in * a class. * * The returned element should be appended to <code>xs:sequence</code> * element of the xs:element representing the type of the owning object. */ Element createXsElementForNofValue(final Element parentXsElementElement, final Element xmlValueElement, final Hashtable extensions) { // gather details from XML element final String datatype = xmlValueElement.getAttributeNS(IsisSchema.NS_URI, "datatype"); final String fieldName = xmlValueElement.getLocalName(); // <xs:element name="%owning object%"> // <xs:complexType> // <xs:sequence> // <xs:element name="%%field object%%"> // <xs:complexType> // <xs:sequence> // <xs:element name="isis-extensions"> // <xs:complexType> // <xs:sequence> // <xs:element name="%extensionClassShortName%" // default="%extensionObjString" minOccurs="0"/> // <xs:element name="%extensionClassShortName%" // default="%extensionObjString" minOccurs="0"/> // ... // <xs:element name="%extensionClassShortName%" // default="%extensionObjString" minOccurs="0"/> // </xs:sequence> // </xs:complexType> // </xs:element> // </xs:sequence> // <xs:attribute ref="isis:feature" fixed="value"/> // <xs:attribute ref="isis:datatype" fixed="isis:%datatype%"/> // <xs:attribute ref="isis:isEmpty"/> // <xs:attribute ref="isis:annotation"/> // </xs:complexType> // </xs:element> // </xs:sequence> // </xs:complexType> // </xs:element> // xs:element/xs:complexType/xs:sequence final Element parentXsComplexTypeElement = xsMeta.complexTypeFor(parentXsElementElement); final Element parentXsSequenceElement = xsMeta.sequenceFor(parentXsComplexTypeElement); // xs:element/xs:complexType/xs:sequence/xs:element // name="%%field object%" final Element xsFieldElementElement = xsMeta.createXsElementElement(helper.docFor(parentXsSequenceElement), fieldName); parentXsSequenceElement.appendChild(xsFieldElementElement); // xs:element/xs:complexType/xs:sequence/xs:element/xs:complexType final Element xsFieldComplexTypeElement = xsMeta.complexTypeFor(xsFieldElementElement); // NEW CODE TO SUPPORT EXTENSIONS; // uses a complexType/sequence // xs:element/xs:complexType/xs:sequence/xs:element/xs:complexType/xs:sequence final Element xsFieldSequenceElement = xsMeta.sequenceFor(xsFieldComplexTypeElement); // xs:element/xs:complexType/xs:sequence/xs:element/xs:complexType/xs:sequence/xs:element // name="isis-extensions" // xs:element/xs:complexType/xs:sequence/xs:element/xs:complexType/xs:sequence/xs:element/xs:complexType/xs:sequence addXsElementForAppExtensions(xsFieldSequenceElement, extensions); xsMeta.addXsIsisFeatureAttributeElements(xsFieldComplexTypeElement, "value"); xsMeta.addXsIsisAttribute(xsFieldComplexTypeElement, "datatype", datatype); xsMeta.addXsIsisAttribute(xsFieldComplexTypeElement, "isEmpty"); xsMeta.addXsIsisAttribute(xsFieldComplexTypeElement, "annotation"); return xsFieldElementElement; } private void addExtensionElements(final Element parentElement, final Hashtable extensions) { for (final Enumeration e = extensions.keys(); e.hasMoreElements();) { final Class<?> extensionClass = (Class<?>) e.nextElement(); final Object extensionObject = extensions.get(extensionClass); // xs:element/xs:complexType/xs:sequence/xs:element/xs:complexType/xs:sequence/xs:element // name="%extensionClassShortName%" final Element xsExtensionElementElement = xsMeta.createXsElementElement(helper.docFor(parentElement), "x-" + shortName(extensionClass.getName())); xsExtensionElementElement.setAttribute("default", extensionObject.toString()); // the // value xsExtensionElementElement.setAttribute("minOccurs", "0"); // doesn't // need to // appear // in XML // (and // indeed won't) parentElement.appendChild(xsExtensionElementElement); } } /** * Creates an <xs:element> element defining the presence of the named * element representing a reference to a class; appended to xs:sequence * element */ Element createXsElementForNofReference(final Element parentXsElementElement, final Element xmlReferenceElement, final String referencedClassName, final Hashtable extensions) { // gather details from XML element final String fieldName = xmlReferenceElement.getLocalName(); // <xs:element name="%owning object%"> // <xs:complexType> // <xs:sequence> // <xs:element name="%%field object%%"> // <xs:complexType> // <xs:sequence> // <xs:element ref="isis:title" minOccurs="0"/> // <xs:element name="isis-extensions"> // <xs:complexType> // <xs:sequence> // <xs:element name="app:%extension class short name%" minOccurs="0" // maxOccurs="1" default="%value%"/> // <xs:element name="app:%extension class short name%" minOccurs="0" // maxOccurs="1" default="%value%"/> // ... // <xs:element name="app:%extension class short name%" minOccurs="0" // maxOccurs="1" default="%value%"/> // </xs:sequence> // </xs:complexType> // </xs:element> // <xs:sequence minOccurs="0" maxOccurs="1"/> // </xs:sequence> // <xs:attribute ref="isis:feature" fixed="reference"/> // <xs:attribute ref="isis:type" default="%%appX%%:%%type%%"/> // <xs:attribute ref="isis:isEmpty"/> // <xs:attribute ref="isis:annotation"/> // </xs:complexType> // </xs:element> // </xs:sequence> // </xs:complexType> // </xs:element> // xs:element/xs:complexType/xs:sequence final Element parentXsComplexTypeElement = xsMeta.complexTypeFor(parentXsElementElement); final Element parentXsSequenceElement = xsMeta.sequenceFor(parentXsComplexTypeElement); // xs:element/xs:complexType/xs:sequence/xs:element // name="%%field object%" final Element xsFieldElementElement = xsMeta.createXsElementElement(helper.docFor(parentXsSequenceElement), fieldName); parentXsSequenceElement.appendChild(xsFieldElementElement); // xs:element/xs:complexType/xs:sequence/xs:element/xs:complexType/xs:sequence final Element xsFieldComplexTypeElement = xsMeta.complexTypeFor(xsFieldElementElement); final Element xsFieldSequenceElement = xsMeta.sequenceFor(xsFieldComplexTypeElement); // xs:element/xs:complexType/xs:sequence/xs:element/xs:complexType/xs:sequence/xs:element // ref="isis:title" final Element xsFieldTitleElement = xsMeta.createXsElement(helper.docFor(xsFieldSequenceElement), "element"); xsFieldTitleElement.setAttribute("ref", IsisSchema.NS_PREFIX + ":" + "title"); xsFieldSequenceElement.appendChild(xsFieldTitleElement); xsMeta.setXsCardinality(xsFieldTitleElement, 0, 1); // xs:element/xs:complexType/xs:sequence/xs:element/xs:complexType/xs:sequence/xs:element // name="isis-extensions" addXsElementForAppExtensions(xsFieldSequenceElement, extensions); // xs:element/xs:complexType/xs:sequence/xs:element/xs:complexType/xs:sequence/xs:sequence // // // placeholder final Element xsReferencedElementSequenceElement = xsMeta.sequenceFor(xsFieldSequenceElement); xsMeta.setXsCardinality(xsReferencedElementSequenceElement, 0, 1); xsMeta.addXsIsisFeatureAttributeElements(xsFieldComplexTypeElement, "reference"); xsMeta.addXsIsisAttribute(xsFieldComplexTypeElement, "type", "app:" + referencedClassName, false); xsMeta.addXsIsisAttribute(xsFieldComplexTypeElement, "isEmpty"); xsMeta.addXsIsisAttribute(xsFieldComplexTypeElement, "annotation"); return xsFieldElementElement; } /** * Creates an <xs:element> element defining the presence of the named * element representing a collection in a class; appended to xs:sequence * element */ Element createXsElementForNofCollection(final Element parentXsElementElement, final Element xmlCollectionElement, final String referencedClassName, final Hashtable extensions) { // gather details from XML element final String fieldName = xmlCollectionElement.getLocalName(); // <xs:element name="%owning object%"> // <xs:complexType> // <xs:sequence> // <xs:element name="%%field object%%"> // <xs:complexType> // <xs:sequence> // <xs:element ref="isis:oids" minOccurs="0" maxOccurs="1"/> // <!-- nested element definitions go here --> // </xs:sequence> // <xs:attribute ref="isis:feature" fixed="collection"/> // <xs:attribute ref="isis:type" fixed="%%appX%%:%%type%%"/> // <xs:attribute ref="isis:size"/> // <xs:attribute ref="isis:annotation"/> // </xs:complexType> // </xs:element> // </xs:sequence> // </xs:complexType> // </xs:element> // xs:element/xs:complexType/xs:sequence final Element parentXsComplexTypeElement = xsMeta.complexTypeFor(parentXsElementElement); final Element parentXsSequenceElement = xsMeta.sequenceFor(parentXsComplexTypeElement); // xs:element/xs:complexType/xs:sequence/xs:element // name="%field object%%" final Element xsFieldElementElement = xsMeta.createXsElementElement(helper.docFor(parentXsSequenceElement), fieldName); parentXsSequenceElement.appendChild(xsFieldElementElement); // xs:element/xs:complexType/xs:sequence/xs:element/xs:complexType final Element xsFieldComplexTypeElement = xsMeta.complexTypeFor(xsFieldElementElement); // xs:element/xs:complexType/xs:sequence/xs:element/xs:complexType/xs:sequence final Element xsFieldSequenceElement = xsMeta.sequenceFor(xsFieldComplexTypeElement); // xs:element/xs:complexType/xs:sequence/xs:element/xs:complexType/xs:sequence/xs:element // ref="isis:oids" final Element xsFieldOidsElement = xsMeta.createXsElement(helper.docFor(xsFieldSequenceElement), "element"); xsFieldOidsElement.setAttribute("ref", IsisSchema.NS_PREFIX + ":" + "oids"); xsFieldSequenceElement.appendChild(xsFieldOidsElement); xsMeta.setXsCardinality(xsFieldOidsElement, 0, 1); // extensions addXsElementForAppExtensions(xsFieldSequenceElement, extensions); // // // xs:element/xs:complexType/xs:sequence/xs:element/xs:complexType/xs:sequence/xs:choice // Element xsFieldChoiceElement = // xsMeta.choiceFor(xsFieldComplexTypeElement); // placeholder // xsMeta.setXsCardinality(xsFieldChoiceElement, 0, Integer.MAX_VALUE); // Element xsFieldTitleElement = // addXsNofRefElementElement(xsFieldSequenceElement, "title"); // Element xsReferencedElementSequenceElement = // sequenceFor(xsFieldSequenceElement); // setXsCardinality(xsReferencedElementSequenceElement, 0, 1); xsMeta.addXsIsisFeatureAttributeElements(xsFieldComplexTypeElement, "collection"); xsMeta.addXsIsisAttribute(xsFieldComplexTypeElement, "type", "app:" + referencedClassName, false); xsMeta.addXsIsisAttribute(xsFieldComplexTypeElement, "size"); xsMeta.addXsIsisAttribute(xsFieldComplexTypeElement, "annotation"); return xsFieldElementElement; } /** * * <pre> * xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" * xsi:schemaLocation="http://isis.apache.org/ns/app/sdm.common.fixture.schemes.ao.communications ddd.xsd" * </pre> * * Assumes that the URI has been specified. * * @param xmlDoc * @param fullyQualifiedClassName * @param schemaLocationFileName */ void assignSchema(final Document xmlDoc, final String fullyQualifiedClassName, final String schemaLocationFileName) { final String xsiSchemaLocationAttrValue = getUri() + " " + schemaLocationFileName; final Element rootElement = xmlDoc.getDocumentElement(); // xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" addNamespace(rootElement, XsMetaModel.W3_ORG_XSI_PREFIX, XsMetaModel.W3_ORG_XSI_URI); // xsi:schemaLocation="http://isis.apache.org/ns/app/<fully qualified // class name> // sdm.common.fixture.schemes.ao.communications // sdm.common.fixture.schemes.ao.communications.AO11ConfirmAnimalRegistration.xsd" rootElement.setAttributeNS(XsMetaModel.W3_ORG_XSI_URI, "xsi:schemaLocation", xsiSchemaLocationAttrValue); } /** * Adds a previously created <xs:element> element (representing a * field of an object) to the supplied element (presumed to be a * <code>complexType/sequence</code>). */ void addFieldXsElement(final Element xsElement, final Element xsFieldElement) { if (xsFieldElement == null) { return; } final Element sequenceElement = xsMeta.sequenceForComplexTypeFor(xsElement); sequenceElement.appendChild(xsFieldElement); } /** * Adds a namespace using the supplied prefix and the supplied URI to the * root element of the document that is the parent of the supplied element. * * If the namespace declaration already exists but has a different URI * (shouldn't normally happen) overwrites with supplied URI. */ private void addNamespace(final Element element, final String prefix, final String nsUri) { final Element rootElement = helper.rootElementFor(element); // see if we have the NS prefix there already final String existingNsUri = rootElement.getAttributeNS(XsMetaModel.W3_ORG_XMLNS_URI, prefix); // if there is none (or it is different from what we want), then set the // attribute if (existingNsUri == null || !existingNsUri.equals(nsUri)) { helper.rootElementFor(element).setAttributeNS(XsMetaModel.W3_ORG_XMLNS_URI, XsMetaModel.W3_ORG_XMLNS_PREFIX + ":" + prefix, nsUri); } } Element addXsElementIfNotPresent(final Element parentXsElement, final Element childXsElement) { final Element parentChoiceOrSequenceElement = xsMeta.choiceOrSequenceFor(xsMeta.complexTypeFor(parentXsElement)); if (parentChoiceOrSequenceElement == null) { throw new IllegalArgumentException("Unable to locate complexType/sequence or complexType/choice under supplied parent XSD element"); } final NamedNodeMap childXsElementAttributeMap = childXsElement.getAttributes(); final Attr childXsElementAttr = (Attr) childXsElementAttributeMap.getNamedItem("name"); final String localName = childXsElementAttr.getValue(); final NodeList existingElements = parentChoiceOrSequenceElement.getElementsByTagNameNS("*", childXsElement.getLocalName()); for (int i = 0; i < existingElements.getLength(); i++) { final Element xsElement = (Element) existingElements.item(i); final NamedNodeMap xsElementAttributeMap = xsElement.getAttributes(); final Attr attr = (Attr) xsElementAttributeMap.getNamedItem("name"); if (attr != null && attr.getValue().equals(localName)) { return xsElement; } } parentChoiceOrSequenceElement.appendChild(childXsElement); return childXsElement; } }