/** * 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.cxf.javascript.types; import java.util.List; import java.util.Map; import java.util.logging.Logger; import javax.xml.namespace.QName; import org.apache.cxf.common.i18n.Message; import org.apache.cxf.common.logging.LogUtils; import org.apache.cxf.common.util.StringUtils; import org.apache.cxf.common.xmlschema.SchemaCollection; import org.apache.cxf.common.xmlschema.XmlSchemaUtils; import org.apache.cxf.javascript.AttributeInfo; import org.apache.cxf.javascript.ItemInfo; import org.apache.cxf.javascript.JavascriptUtils; import org.apache.cxf.javascript.NameManager; import org.apache.cxf.javascript.NamespacePrefixAccumulator; import org.apache.cxf.javascript.ParticleInfo; import org.apache.cxf.javascript.UnsupportedConstruct; import org.apache.ws.commons.schema.XmlSchema; import org.apache.ws.commons.schema.XmlSchemaAnnotated; import org.apache.ws.commons.schema.XmlSchemaAny; import org.apache.ws.commons.schema.XmlSchemaAttributeOrGroupRef; import org.apache.ws.commons.schema.XmlSchemaCollection; import org.apache.ws.commons.schema.XmlSchemaComplexType; import org.apache.ws.commons.schema.XmlSchemaElement; import org.apache.ws.commons.schema.XmlSchemaObject; import org.apache.ws.commons.schema.XmlSchemaSimpleType; import org.apache.ws.commons.schema.XmlSchemaType; import org.apache.ws.commons.schema.constants.Constants; /** * Generate Javascript for a schema, and provide information needed for the service builder. As of this pass, * there is no support for non-sequence types or for attribute mappings. */ public class SchemaJavascriptBuilder { private static final Logger LOG = LogUtils.getL7dLogger(SchemaJavascriptBuilder.class); private static int anyPrefixCounter; private SchemaCollection xmlSchemaCollection; private NameManager nameManager; private NamespacePrefixAccumulator prefixAccumulator; // private SchemaInfo schemaInfo; // In general, I (bimargulies) hate fields that are temporary communications // between members of a class. However, given the style restrictions on the // number of parameters, it's the least of the evils. private StringBuilder code; private StringBuilder accessors; private JavascriptUtils utils; private XmlSchema xmlSchema; public SchemaJavascriptBuilder(SchemaCollection schemaCollection, NamespacePrefixAccumulator prefixAccumulator, NameManager nameManager) { this.xmlSchemaCollection = schemaCollection; this.nameManager = nameManager; this.prefixAccumulator = prefixAccumulator; } public String generateCodeForSchemaCollection(XmlSchemaCollection collection) { StringBuilder accumulatedCode = new StringBuilder(); for (XmlSchema schema : collection.getXmlSchemas()) { if (!Constants.URI_2001_SCHEMA_XSD.equals(schema.getTargetNamespace())) { accumulatedCode.append(generateCodeForSchema(schema)); } } return accumulatedCode.toString(); } public String generateCodeForSchema(XmlSchema schema) { xmlSchema = schema; code = new StringBuilder(); code.append("//\n"); code.append("// Definitions for schema: " + schema.getTargetNamespace()); if (schema.getSourceURI() != null) { code.append("\n// " + schema.getSourceURI()); } code.append("\n//\n"); Map<QName, XmlSchemaType> schemaTypes = schema.getSchemaTypes(); for (Map.Entry<QName, XmlSchemaType> e : schemaTypes.entrySet()) { XmlSchemaType type = e.getValue(); if (type instanceof XmlSchemaComplexType) { try { XmlSchemaComplexType complexType = (XmlSchemaComplexType)type; if (!JavascriptUtils.notVeryComplexType(complexType) && complexType.getName() != null) { complexTypeConstructorAndAccessors(complexType.getQName(), complexType); complexTypeSerializerFunction(complexType.getQName(), complexType); domDeserializerFunction(complexType.getQName(), complexType); } } catch (UnsupportedConstruct usc) { LOG.warning(usc.toString()); continue; // it could be empty, but the style checker // would complain. } } else if (type instanceof XmlSchemaSimpleType) { XmlSchemaSimpleType simpleType = (XmlSchemaSimpleType)type; if (XmlSchemaUtils.isEumeration(simpleType)) { List<String> values = XmlSchemaUtils.enumeratorValues(simpleType); code.append("//\n"); code.append("// Simple type (enumeration) " + simpleType.getQName() + "\n"); code.append("//\n"); for (String value : values) { code.append("// - " + value + "\n"); } } } } for (Map.Entry<QName, XmlSchemaElement> e : schema.getElements().entrySet()) { XmlSchemaElement element = e.getValue(); try { if (element.getSchemaTypeName() == null && element.getSchemaType() == null) { Message message = new Message("ELEMENT_MISSING_TYPE", LOG, element.getQName(), element.getSchemaTypeName(), schema.getTargetNamespace()); LOG.warning(message.toString()); continue; } XmlSchemaType type; if (element.getSchemaType() != null) { type = element.getSchemaType(); } else { type = schema.getTypeByName(element.getSchemaTypeName()); } if (!(type instanceof XmlSchemaComplexType)) { // we never make classes for simple type. continue; } XmlSchemaComplexType complexType = (XmlSchemaComplexType)type; // for named types we don't bother to generate for the // element. if (!JavascriptUtils.notVeryComplexType(complexType) && complexType.getName() == null) { complexTypeConstructorAndAccessors(element.getQName(), complexType); complexTypeSerializerFunction(element.getQName(), complexType); domDeserializerFunction(element.getQName(), complexType); } } catch (UnsupportedConstruct usc) { LOG.warning(usc.getMessage()); continue; // it could be empty, but the style checker // would complain. } } String returnValue = code.toString(); LOG.finer(returnValue); return returnValue; } // In general, I (bimargulies) hate fields that are temporary communications // between members of a class. However, given the style restrictions on the // number // of parameters, it's the least of the evils. public void complexTypeConstructorAndAccessors(QName name, XmlSchemaComplexType type) { accessors = new StringBuilder(); utils = new JavascriptUtils(code); List<XmlSchemaObject> items = JavascriptUtils.getContentElements(type, xmlSchemaCollection); List<XmlSchemaAnnotated> attrs = XmlSchemaUtils.getContentAttributes(type, xmlSchemaCollection); final String elementPrefix = "this._"; String typeObjectName = nameManager.getJavascriptName(name); code.append("//\n"); code.append("// Constructor for XML Schema item " + name.toString() + "\n"); code.append("//\n"); code.append("function " + typeObjectName + " () {\n"); // to assist in debugging we put a type property into every object. utils.appendLine("this.typeMarker = '" + typeObjectName + "';"); for (XmlSchemaObject thing : items) { ParticleInfo itemInfo = ParticleInfo.forLocalItem(thing, xmlSchema, xmlSchemaCollection, prefixAccumulator, type.getQName()); constructItem(type, elementPrefix, typeObjectName, itemInfo); } for (XmlSchemaAnnotated thing : attrs) { AttributeInfo itemInfo = AttributeInfo.forLocalItem(thing, xmlSchema, xmlSchemaCollection, prefixAccumulator, type.getQName()); constructOneItem(type, elementPrefix, typeObjectName, itemInfo); } code.append("}\n\n"); code.append(accessors.toString()); } private void constructItem(XmlSchemaComplexType type, final String elementPrefix, String typeObjectName, ParticleInfo itemInfo) { if (!itemInfo.isGroup()) { constructOneItem(type, elementPrefix, typeObjectName, itemInfo); return; } for (ParticleInfo childInfo : itemInfo.getChildren()) { constructItem(type, elementPrefix, typeObjectName, childInfo); } } private void constructOneItem(XmlSchemaComplexType type, final String elementPrefix, String typeObjectName, ItemInfo itemInfo) { String accessorSuffix = StringUtils.capitalize(itemInfo.getJavascriptName()); String accessorName = typeObjectName + "_get" + accessorSuffix; String getFunctionProperty = typeObjectName + ".prototype.get" + accessorSuffix; String setFunctionProperty = typeObjectName + ".prototype.set" + accessorSuffix; accessors.append("//\n"); accessors.append("// accessor is " + getFunctionProperty + "\n"); accessors.append("// element get for " + itemInfo.getJavascriptName() + "\n"); if (itemInfo.isAny()) { accessors.append("// - xs:any\n"); } else { if (itemInfo.getType() != null) { accessors.append("// - element type is " + itemInfo.getType().getQName() + "\n"); } } if (itemInfo.isOptional()) { accessors.append("// - optional element\n"); } else { accessors.append("// - required element\n"); } if (itemInfo.isArray()) { accessors.append("// - array\n"); } if (itemInfo.isNillable()) { accessors.append("// - nillable\n"); } accessors.append("//\n"); accessors.append("// element set for " + itemInfo.getJavascriptName() + "\n"); accessors.append("// setter function is is " + setFunctionProperty + "\n"); accessors.append("//\n"); accessors.append("function " + accessorName + "() { return this._" + itemInfo.getJavascriptName() + ";}\n\n"); accessors.append(getFunctionProperty + " = " + accessorName + ";\n\n"); accessorName = typeObjectName + "_set" + accessorSuffix; accessors.append("function " + accessorName + "(value) { this._" + itemInfo.getJavascriptName() + " = value;}\n\n"); accessors.append(setFunctionProperty + " = " + accessorName + ";\n"); if (itemInfo.isOptional() || (itemInfo.isNillable() && !itemInfo.isArray())) { utils.appendLine("this._" + itemInfo.getJavascriptName() + " = null;"); } else if (itemInfo.isArray()) { utils.appendLine("this._" + itemInfo.getJavascriptName() + " = [];"); } else if (itemInfo.isAny() || itemInfo.getType() instanceof XmlSchemaComplexType) { // even for required complex elements, we leave them null. // otherwise, we could end up in a cycle or otherwise miserable. The // application code is responsible for this. utils.appendLine("this._" + itemInfo.getJavascriptName() + " = null;"); } else { if (itemInfo.getDefaultValue() == null) { itemInfo.setDefaultValue(utils.getDefaultValueForSimpleType(itemInfo.getType())); } utils.appendLine("this._" + itemInfo.getJavascriptName() + " = " + itemInfo.getDefaultValue() + ";"); } } /** * Produce a serializer function for a type. These functions emit the surrounding element XML if the * caller supplies an XML element name. It's not quite as simple as that, though. The element name may * need namespace qualification, and this function will add more namespace prefixes as needed. * * @param type * @return */ public void complexTypeSerializerFunction(QName name, XmlSchemaComplexType type) { StringBuilder bodyCode = new StringBuilder(); JavascriptUtils bodyUtils = new JavascriptUtils(bodyCode); bodyUtils.setXmlStringAccumulator("xml"); complexTypeSerializerBody(type, "this._", bodyUtils); utils = new JavascriptUtils(code); String functionName = nameManager.getJavascriptName(name) + "_" + "serialize"; code.append("//\n"); code.append("// Serialize " + name + "\n"); code.append("//\n"); code.append("function " + functionName + "(cxfjsutils, elementName, extraNamespaces) {\n"); utils.startXmlStringAccumulator("xml"); utils.startIf("elementName !== null"); utils.appendString("<"); utils.appendExpression("elementName"); // now add any accumulated namespaces. String moreNamespaces = prefixAccumulator.getAttributes(); if (moreNamespaces.length() > 0) { utils.appendString(" "); utils.appendString(moreNamespaces); } utils.startIf("extraNamespaces"); utils.appendExpression("' ' + extraNamespaces"); utils.endBlock(); // attributes complexTypeSerializeAttributes(type, "this._"); utils.appendString(">"); utils.endBlock(); code.append(bodyCode); utils.startIf("elementName !== null"); utils.appendString("</"); utils.appendExpression("elementName"); utils.appendString(">"); utils.endBlock(); utils.appendLine("return xml;"); code.append("}\n\n"); code.append(nameManager.getJavascriptName(name) + ".prototype.serialize = " + functionName + ";\n\n"); } private void complexTypeSerializeAttributes(XmlSchemaComplexType type, String string) { @SuppressWarnings("unused") List<XmlSchemaAttributeOrGroupRef> attributes = type.getAttributes(); // work in progress. } /** * Build the serialization code for a complex type. At the top level, this operates on single items, so it * does not pay attention to minOccurs and maxOccurs. However, as it works through the sequence, it * manages optional elements and arrays. * * @param type * @param elementPrefix * @param bodyNamespaceURIs * @return */ protected void complexTypeSerializerBody(XmlSchemaComplexType type, String elementPrefix, JavascriptUtils bodyUtils) { List<XmlSchemaObject> items = JavascriptUtils.getContentElements(type, xmlSchemaCollection); for (XmlSchemaObject sequenceItem : items) { ParticleInfo itemInfo = ParticleInfo.forLocalItem(sequenceItem, xmlSchema, xmlSchemaCollection, prefixAccumulator, type.getQName()); // If the item is 'any', it could be ANY of our top-level elements. if (itemInfo.isAny()) { serializeAny(itemInfo, bodyUtils); } else { bodyUtils.generateCodeToSerializeElement(itemInfo, "this._", xmlSchemaCollection); } } } private void serializeAny(ParticleInfo itemInfo, JavascriptUtils bodyUtils) { String prefix = "cxfjsany" + anyPrefixCounter; anyPrefixCounter++; bodyUtils.generateCodeToSerializeAny(itemInfo, prefix, xmlSchemaCollection); } /** * Generate a JavaScript function that takes an element for a complex type and walks through its children * using them to fill in the values for a JavaScript object. * * @param type schema type for the process * @return the string contents of the JavaScript. */ public void domDeserializerFunction(QName name, XmlSchemaComplexType type) { utils = new JavascriptUtils(code); List<XmlSchemaObject> contentElements = JavascriptUtils.getContentElements(type, xmlSchemaCollection); String typeObjectName = nameManager.getJavascriptName(name); code.append("function " + typeObjectName + "_deserialize (cxfjsutils, element) {\n"); // create the object we are deserializing into. utils.appendLine("var newobject = new " + typeObjectName + "();"); utils.appendLine("cxfjsutils.trace('element: ' + cxfjsutils.traceElementName(element));"); utils.appendLine("var curElement = cxfjsutils.getFirstElementChild(element);"); utils.appendLine("var item;"); int nContentElements = contentElements.size(); for (int i = 0; i < contentElements.size(); i++) { XmlSchemaObject contentElement = contentElements.get(i); utils.appendLine("cxfjsutils.trace('curElement: ' + cxfjsutils.traceElementName(curElement));"); ParticleInfo itemInfo = ParticleInfo.forLocalItem(contentElement, xmlSchema, xmlSchemaCollection, prefixAccumulator, type.getQName()); if (itemInfo.isAny()) { ParticleInfo nextItem = null; if (i != nContentElements - 1) { XmlSchemaObject nextThing = contentElements.get(i + 1); nextItem = ParticleInfo.forLocalItem(nextThing, xmlSchema, xmlSchemaCollection, prefixAccumulator, type.getQName()); // theoretically, you could have two anys with different // namespaces. if (nextItem.isAny()) { unsupportedConstruct("MULTIPLE_ANY", type.getQName()); } } deserializeAny(type, itemInfo, nextItem); } else { deserializeElement(type, itemInfo); } } utils.appendLine("return newobject;"); code.append("}\n\n"); } private String buildNamespaceList(String anyNamespaceSpec) { StringBuilder nslist = new StringBuilder(); String[] namespaces = anyNamespaceSpec.split("\\s"); nslist.append("[ "); for (int x = 0; x < namespaces.length; x++) { String ns = namespaces[x]; nslist.append("'"); if ("##targetNamespace".equals(ns)) { nslist.append(xmlSchema.getTargetNamespace()); } else if ("##local".equals(ns)) { // nothing, empty string } else { nslist.append(ns); } nslist.append("'"); if (x < namespaces.length - 1) { nslist.append(","); } } nslist.append("]"); return nslist.toString(); } private void deserializeAny(XmlSchemaComplexType type, ParticleInfo itemInfo, ParticleInfo nextItem) { XmlSchemaAny any = (XmlSchemaAny)itemInfo.getParticle(); boolean array = XmlSchemaUtils.isParticleArray(any); boolean optional = XmlSchemaUtils.isParticleOptional(any); if (array) { utils.appendLine("var anyObject = [];"); } else { utils.appendLine("var anyObject = null;"); } String anyNamespaceSpec = any.getNamespace(); // we aren't dealing with any-after-any. XmlSchemaElement nextElement = null; if (nextItem != null) { nextElement = (XmlSchemaElement)nextItem.getParticle(); } String matchType; String namespaceList = "[]"; if (anyNamespaceSpec == null || "##any".equals(anyNamespaceSpec) || "".equals(anyNamespaceSpec)) { matchType = "org_apache_cxf_any_ns_matcher.ANY"; } else if ("##other".equals(anyNamespaceSpec)) { matchType = "org_apache_cxf_any_ns_matcher.OTHER"; } else if ("##local".equals(anyNamespaceSpec)) { matchType = "org_apache_cxf_any_ns_matcher.LOCAL"; } else { matchType = "org_apache_cxf_any_ns_matcher.LISTED"; namespaceList = buildNamespaceList(anyNamespaceSpec); } String nextLocalPartConstant = "null"; if (nextElement != null) { nextLocalPartConstant = "'" + nextElement.getQName().getLocalPart() + "'"; } utils.appendLine("var matcher = new org_apache_cxf_any_ns_matcher(" + matchType + ", '" + xmlSchema.getTargetNamespace() + "'" + ", " + namespaceList + ", " + nextLocalPartConstant + ");"); if (array) { utils.appendLine("var anyNeeded = " + any.getMinOccurs() + ";"); utils.appendLine("var anyAllowed = " + any.getMaxOccurs() + ";"); } else if (optional) { utils.appendLine("var anyNeeded = 0;"); utils.appendLine("var anyAllowed = 1;"); } else { utils.appendLine("var anyNeeded = 1;"); utils.appendLine("var anyAllowed = 1;"); } utils.startWhile("anyNeeded > 0 || anyAllowed > 0"); utils.appendLine("var anyURI;"); utils.appendLine("var anyLocalPart;"); utils.appendLine("var anyMatched = false;"); utils.startIf("curElement"); utils.appendLine("anyURI = cxfjsutils.getElementNamespaceURI(curElement);"); utils.appendLine("anyLocalPart = cxfjsutils.getNodeLocalName(curElement);"); utils.appendLine("var anyQName = '{' + anyURI + '}' + anyLocalPart;"); utils.appendLine("cxfjsutils.trace('any match: ' + anyQName);"); utils.appendLine("anyMatched = matcher.match(anyURI, anyLocalPart)"); utils.appendLine("cxfjsutils.trace(' --> ' + anyMatched);"); utils.endBlock(); // curElement != null utils.startIf("anyMatched"); // if match utils.appendLine("anyDeserializer = " + "cxfjsutils.interfaceObject.globalElementDeserializers[anyQName];"); utils.appendLine("cxfjsutils.trace(' deserializer: ' + anyDeserializer);"); utils.startIf("anyDeserializer"); // if complex/serializer function utils.appendLine("var anyValue = anyDeserializer(cxfjsutils, curElement);"); utils.appendElse(); // else complex/serializer function // TODO: for simple types we really need a dictionary of the simple type qnames. utils.appendLine("var anyValue = curElement.nodeValue;"); utils.endBlock(); // complex/serializer function if (array) { utils.appendLine("anyObject.push(anyValue);"); } else { utils.appendLine("anyObject = anyValue;"); } utils.appendLine("anyNeeded--;"); utils.appendLine("anyAllowed--;"); // if we consumed the element, we advance. utils.appendLine("curElement = cxfjsutils.getNextElementSibling(curElement);"); utils.appendElse(); // match // non-matching case utils.startIf("anyNeeded > 0"); utils.appendLine("throw 'not enough ws:any elements';"); utils.appendElse(); // else non-match utils.appendLine("break;"); utils.endBlock(); // non-match+required utils.endBlock(); // match/non-match. utils.endBlock(); // while utils.appendLine("var anyHolder = new org_apache_cxf_any_holder(anyURI, anyLocalPart, anyValue);"); utils.appendLine("newobject.setAny(anyHolder);"); } private void deserializeElement(XmlSchemaComplexType type, ParticleInfo itemInfo) { if (itemInfo.isGroup()) { for (ParticleInfo childElement : itemInfo.getChildren()) { deserializeElement(type, childElement); } return; } XmlSchemaType itemType = itemInfo.getType(); boolean simple = itemType instanceof XmlSchemaSimpleType || JavascriptUtils.notVeryComplexType(itemType); boolean mtomCandidate = JavascriptUtils.mtomCandidateType(itemType); String accessorName = "set" + StringUtils.capitalize(itemInfo.getJavascriptName()); utils.appendLine("cxfjsutils.trace('processing " + itemInfo.getJavascriptName() + "');"); XmlSchemaElement element = (XmlSchemaElement)itemInfo.getParticle(); QName elementQName = XmlSchemaUtils.getElementQualifiedName(element, xmlSchema); String elementNamespaceURI = elementQName.getNamespaceURI(); boolean elementNoNamespace = "".equals(elementNamespaceURI); XmlSchema elementSchema = null; if (!elementNoNamespace) { elementSchema = xmlSchemaCollection.getSchemaByTargetNamespace(elementNamespaceURI); } boolean qualified = !elementNoNamespace && XmlSchemaUtils.isElementQualified(element, itemInfo.isGlobal(), xmlSchema, elementSchema); if (!qualified) { elementNamespaceURI = ""; } String localName = elementQName.getLocalPart(); String valueTarget = "item"; if (itemInfo.isOptional() || itemInfo.isArray()) { utils.startIf("curElement != null && cxfjsutils.isNodeNamedNS(curElement, '" + elementNamespaceURI + "', '" + localName + "')"); if (itemInfo.isArray()) { utils.appendLine("item = [];"); utils.startDo(); valueTarget = "arrayItem"; utils.appendLine("var arrayItem = null;"); } } utils.appendLine("var value = null;"); utils.startIf("!cxfjsutils.isElementNil(curElement)"); if (itemInfo.isAnyType()) { // use our utility utils.appendLine(valueTarget + " = org_apache_cxf_deserialize_anyType(cxfjsutils, curElement);"); } else if (simple) { if (mtomCandidate) { utils.appendLine(valueTarget + " = cxfjsutils.deserializeBase64orMom(curElement);"); } else { utils.appendLine("value = cxfjsutils.getNodeText(curElement);"); utils.appendLine(valueTarget + " = " + utils.javascriptParseExpression(itemType, "value") + ";"); } } else { XmlSchemaComplexType complexType = (XmlSchemaComplexType)itemType; QName baseQName = complexType.getQName(); if (baseQName == null) { baseQName = element.getQName(); } String elTypeJsName = nameManager.getJavascriptName(baseQName); utils.appendLine(valueTarget + " = " + elTypeJsName + "_deserialize(cxfjsutils, curElement);"); } utils.endBlock(); // the if for the nil. if (itemInfo.isArray()) { utils.appendLine("item.push(arrayItem);"); utils.appendLine("curElement = cxfjsutils.getNextElementSibling(curElement);"); utils.endBlock(); utils.appendLine(" while(curElement != null && cxfjsutils.isNodeNamedNS(curElement, '" + elementNamespaceURI + "', '" + localName + "'));"); } utils.appendLine("newobject." + accessorName + "(item);"); utils.appendLine("var item = null;"); if (!itemInfo.isArray()) { utils.startIf("curElement != null"); utils.appendLine("curElement = cxfjsutils.getNextElementSibling(curElement);"); utils.endBlock(); } if (itemInfo.isOptional() || itemInfo.isArray()) { utils.endBlock(); } } private void unsupportedConstruct(String messageKey, Object... args) { Message message = new Message(messageKey, LOG, args); throw new UnsupportedConstruct(message); } }