/** * 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; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Stack; import java.util.logging.Logger; import javax.xml.namespace.QName; import org.w3c.dom.Attr; import org.apache.cxf.common.i18n.Message; import org.apache.cxf.common.logging.LogUtils; import org.apache.cxf.common.xmlschema.SchemaCollection; import org.apache.cxf.common.xmlschema.XmlSchemaUtils; import org.apache.cxf.databinding.source.mime.MimeAttribute; import org.apache.cxf.wsdl.WSDLConstants; import org.apache.ws.commons.schema.XmlSchema; import org.apache.ws.commons.schema.XmlSchemaAll; import org.apache.ws.commons.schema.XmlSchemaAnnotated; import org.apache.ws.commons.schema.XmlSchemaAny; import org.apache.ws.commons.schema.XmlSchemaAnyAttribute; import org.apache.ws.commons.schema.XmlSchemaAttribute; import org.apache.ws.commons.schema.XmlSchemaChoice; import org.apache.ws.commons.schema.XmlSchemaComplexContentExtension; import org.apache.ws.commons.schema.XmlSchemaComplexType; import org.apache.ws.commons.schema.XmlSchemaContent; import org.apache.ws.commons.schema.XmlSchemaContentModel; import org.apache.ws.commons.schema.XmlSchemaElement; import org.apache.ws.commons.schema.XmlSchemaGroup; import org.apache.ws.commons.schema.XmlSchemaGroupRef; import org.apache.ws.commons.schema.XmlSchemaObject; import org.apache.ws.commons.schema.XmlSchemaParticle; import org.apache.ws.commons.schema.XmlSchemaSequence; import org.apache.ws.commons.schema.XmlSchemaSequenceMember; import org.apache.ws.commons.schema.XmlSchemaSimpleContent; import org.apache.ws.commons.schema.XmlSchemaSimpleContentExtension; import org.apache.ws.commons.schema.XmlSchemaSimpleType; import org.apache.ws.commons.schema.XmlSchemaType; import org.apache.ws.commons.schema.constants.Constants; /** * A set of functions that assist in JavaScript generation. This includes * functions for appending strings of JavaScript to a buffer as well as some * type utilities. */ public class JavascriptUtils { private static final XmlSchemaSequence EMPTY_SEQUENCE = new XmlSchemaSequence(); private static final XmlSchemaChoice EMPTY_CHOICE = new XmlSchemaChoice(); private static final XmlSchemaAll EMPTY_ALL = new XmlSchemaAll(); private static final Logger LOG = LogUtils.getL7dLogger(JavascriptUtils.class); private static final String NL = "\n"; private static int anyTypePrefixCounter; private StringBuilder code; private Stack<String> prefixStack; private String xmlStringAccumulatorVariable; private Map<String, String> defaultValueForSimpleType; private Set<String> nonStringSimpleTypes; private Set<String> intTypes; private Set<String> floatTypes; public JavascriptUtils(StringBuilder code) { this.code = code; defaultValueForSimpleType = new HashMap<>(); defaultValueForSimpleType.put("int", "0"); defaultValueForSimpleType.put("unsignedInt", "0"); defaultValueForSimpleType.put("long", "0"); defaultValueForSimpleType.put("unsignedLong", "0"); defaultValueForSimpleType.put("float", "0.0"); defaultValueForSimpleType.put("double", "0.0"); nonStringSimpleTypes = new HashSet<>(); nonStringSimpleTypes.add("int"); nonStringSimpleTypes.add("long"); nonStringSimpleTypes.add("unsignedInt"); nonStringSimpleTypes.add("unsignedLong"); nonStringSimpleTypes.add("float"); nonStringSimpleTypes.add("double"); intTypes = new HashSet<>(); intTypes.add("int"); intTypes.add("long"); intTypes.add("unsignedInt"); intTypes.add("unsignedLong"); floatTypes = new HashSet<>(); floatTypes.add("float"); floatTypes.add("double"); prefixStack = new Stack<String>(); prefixStack.push(" "); } public String getDefaultValueForSimpleType(XmlSchemaType type) { String val = defaultValueForSimpleType.get(type.getName()); if (val == null) { // ints and such return the appropriate 0. return "''"; } else { return val; } } public boolean isStringSimpleType(QName typeName) { return !(WSDLConstants.NS_SCHEMA_XSD.equals(typeName.getNamespaceURI()) && nonStringSimpleTypes .contains(typeName.getLocalPart())); } public void setXmlStringAccumulator(String variableName) { xmlStringAccumulatorVariable = variableName; } public void startXmlStringAccumulator(String variableName) { xmlStringAccumulatorVariable = variableName; code.append(prefix()); code.append("var "); code.append(variableName); code.append(" = '';" + NL); } public static String protectSingleQuotes(String value) { return value.replaceAll("'", "\\'"); } public String escapeStringQuotes(String data) { return data.replace("'", "\\'"); } /** * emit javascript to append a value to the accumulator. * * @param value */ public void appendString(String value) { code.append(prefix()); code.append(xmlStringAccumulatorVariable + " = " + xmlStringAccumulatorVariable + " + '"); code.append(escapeStringQuotes(value)); code.append("';" + NL); } public void appendExpression(String value) { code.append(prefix()); code.append(xmlStringAccumulatorVariable + " = " + xmlStringAccumulatorVariable + " + "); code.append(value); code.append(";" + NL); } private String prefix() { return prefixStack.peek(); } public void appendLine(String line) { code.append(prefix()); code.append(line); code.append(NL); } public void startIf(String test) { code.append(prefix()); code.append("if (" + test + ") {" + NL); prefixStack.push(prefix() + " "); } public void startBlock() { code.append(prefix()); code.append("{" + NL); prefixStack.push(prefix() + " "); } public void appendElse() { prefixStack.pop(); code.append(prefix()); code.append("} else {" + NL); prefixStack.push(prefix() + " "); } public void endBlock() { prefixStack.pop(); code.append(prefix()); code.append("}" + NL); } public void startFor(String start, String test, String increment) { code.append(prefix()); code.append("for (" + start + ";" + test + ";" + increment + ") {" + NL); prefixStack.push(prefix() + " "); } public void startForIn(String var, String collection) { code.append(prefix()); code.append("for (var " + var + " in " + collection + ") {" + NL); prefixStack.push(prefix() + " "); } public void startWhile(String test) { code.append(prefix()); code.append("while (" + test + ") {" + NL); prefixStack.push(prefix() + " "); } public void startDo() { code.append(prefix()); code.append("do {" + NL); prefixStack.push(prefix() + " "); } // Given a js variable and a simple type object, correctly set the variables // simple type public String javascriptParseExpression(XmlSchemaType type, String value) { if (!(type instanceof XmlSchemaSimpleType)) { return value; } String name = type.getName(); if (intTypes.contains(name)) { return "parseInt(" + value + ")"; } else if (floatTypes.contains(name)) { return "parseFloat(" + value + ")"; } else if ("boolean".equals(name)) { return "(" + value + " == 'true')"; } else { return value; } } public static String javaScriptNameToken(String token) { return token; } /** * We really don't want to take the attitude that 'all base64Binary elements are candidates for MTOM'. * So we look for clues. * @param schemaObject * @return */ private boolean treatAsMtom(XmlSchemaObject schemaObject) { if (schemaObject == null) { return false; } Map<Object, Object> metaInfoMap = schemaObject.getMetaInfoMap(); if (metaInfoMap != null) { Map<?, ?> attribMap = (Map<?, ?>)metaInfoMap.get(Constants.MetaDataConstants.EXTERNAL_ATTRIBUTES); Attr ctAttr = (Attr)attribMap.get(MimeAttribute.MIME_QNAME); if (ctAttr != null) { return true; } } if (schemaObject instanceof XmlSchemaElement) { XmlSchemaElement element = (XmlSchemaElement) schemaObject; if (element.getSchemaType() == null) { return false; } QName typeName = element.getSchemaType().getQName(); // We could do something much more complex in terms of evaluating whether the type // permits the contentType attribute. This, however, is enough to clue us in for what Aegis // does. if (new QName("http://www.w3.org/2005/05/xmlmime", "base64Binary").equals(typeName)) { return true; } } return false; } /** * We don't want to generate Javascript overhead for complex types with simple content models, * at least until or unless we decide to cope with attributes in a general way. * @param type * @return */ public static boolean notVeryComplexType(XmlSchemaType type) { return type instanceof XmlSchemaSimpleType || (type instanceof XmlSchemaComplexType && ((XmlSchemaComplexType)type).getContentModel() instanceof XmlSchemaSimpleContent); } /** * Return true for xsd:base64Binary or simple restrictions of it, as in the xmime stock type. * @param type * @return */ public static boolean mtomCandidateType(XmlSchemaType type) { if (type == null) { return false; } if (Constants.XSD_BASE64.equals(type.getQName())) { return true; } // there could be some disagreement whether the following is a good enough test. // what if 'base64binary' was extended in some crazy way? At runtime, either it has // an xop:Include or it doesn't. if (type instanceof XmlSchemaComplexType) { XmlSchemaComplexType complexType = (XmlSchemaComplexType)type; if (complexType.getContentModel() instanceof XmlSchemaSimpleContent) { XmlSchemaSimpleContent content = (XmlSchemaSimpleContent)complexType.getContentModel(); if (content.getContent() instanceof XmlSchemaSimpleContentExtension) { XmlSchemaSimpleContentExtension extension = (XmlSchemaSimpleContentExtension)content.getContent(); if (Constants.XSD_BASE64.equals(extension.getBaseTypeName())) { return true; } } } } return false; } /** * Given an element, generate the serialization code. * * @param elementInfo description of the element we are serializing * @param referencePrefix prefix to the Javascript variable. Nothing for * args, this._ for members. * @param schemaCollection caller's schema collection. */ public void generateCodeToSerializeElement(ParticleInfo elementInfo, String referencePrefix, SchemaCollection schemaCollection) { if (elementInfo.isGroup()) { for (ParticleInfo childElement : elementInfo.getChildren()) { generateCodeToSerializeElement(childElement, referencePrefix, schemaCollection); } return; } XmlSchemaType type = elementInfo.getType(); boolean nillable = elementInfo.isNillable(); boolean optional = elementInfo.isOptional(); boolean array = elementInfo.isArray(); boolean mtom = treatAsMtom(elementInfo.getParticle()); String jsVar = referencePrefix + elementInfo.getJavascriptName(); appendLine("// block for local variables"); startBlock(); // allow local variables. // first question: optional? if (optional) { startIf(jsVar + " != null"); } // nillable and optional would be very strange together. // and nillable in the array case applies to the elements. if (nillable && !array) { startIf(jsVar + " == null"); appendString("<" + elementInfo.getXmlName() + " " + XmlSchemaUtils.XSI_NIL + "/>"); appendElse(); } if (array) { // protected against null in arrays. startIf(jsVar + " != null"); startFor("var ax = 0", "ax < " + jsVar + ".length", "ax ++"); jsVar = jsVar + "[ax]"; // we need an extra level of 'nil' testing here. Or do we, depending // on the type structure? // Recode and fiddle appropriately. startIf(jsVar + " == null"); if (nillable) { appendString("<" + elementInfo.getXmlName() + " " + XmlSchemaUtils.XSI_NIL + "/>"); } else { appendString("<" + elementInfo.getXmlName() + "/>"); } appendElse(); } if (elementInfo.isAnyType()) { serializeAnyTypeElement(elementInfo, jsVar); // mtom can be turned on for the special complex type that is really a basic type with // a content-type attribute. } else if (!mtom && type instanceof XmlSchemaComplexType) { // it has a value // pass the extra null in the slot for the 'extra namespaces' needed // by 'any'. appendExpression(jsVar + ".serialize(cxfjsutils, '" + elementInfo.getXmlName() + "', null)"); } else { // simple type appendString("<" + elementInfo.getXmlName() + ">"); if (mtom) { appendExpression("cxfjsutils.packageMtom(" + jsVar + ")"); } else { appendExpression("cxfjsutils.escapeXmlEntities(" + jsVar + ")"); } appendString("</" + elementInfo.getXmlName() + ">"); } if (array) { endBlock(); // for the extra level of nil checking, which might be // wrong. endBlock(); // for the for loop. endBlock(); // the null protection. } if (nillable && !array) { endBlock(); } if (optional) { endBlock(); } endBlock(); // local variables } private void serializeAnyTypeElement(ParticleInfo elementInfo, String jsVar) { // name a variable for convenience. appendLine("var anyHolder = " + jsVar + ";"); appendLine("var anySerializer;"); appendLine("var typeAttr = '';"); // we look in the global array for a serializer. startIf("anyHolder != null"); startIf("!anyHolder.raw"); // no serializer for raw. // In anyType, the QName is for the type, not an element. appendLine("anySerializer = " + "cxfjsutils.interfaceObject.globalElementSerializers[anyHolder.qname];"); endBlock(); startIf("anyHolder.xsiType"); appendLine("var typePrefix = 'cxfjst" + anyTypePrefixCounter + "';"); anyTypePrefixCounter++; appendLine("var typeAttr = 'xmlns:' + typePrefix + '=\\\''" + " + anyHolder.namespaceURI + '\\\'';"); appendLine("typeAttr = typeAttr + ' xsi:type=\\\'' + typePrefix + ':' " + "+ anyHolder.localName + '\\\'';"); endBlock(); startIf("anySerializer"); appendExpression(jsVar + ".serialize(cxfjsutils, '" + elementInfo.getXmlName() + "', typeAttr)"); appendElse(); // simple type or raw appendExpression("'<" + elementInfo.getXmlName() + " ' + typeAttr + " + "'>'"); startIf("!anyHolder.raw"); appendExpression("cxfjsutils.escapeXmlEntities(" + jsVar + ")"); appendElse(); appendExpression("anyHolder.xml"); endBlock(); appendString("</" + elementInfo.getXmlName() + ">"); endBlock(); appendElse(); // nil (from null holder) appendString("<" + elementInfo.getXmlName() + " " + XmlSchemaUtils.XSI_NIL + "/>"); endBlock(); } /** * Generate code to serialize an xs:any. There is too much duplicate code * with the element serializer; fix that some day. * * @param elementInfo * @param schemaCollection */ public void generateCodeToSerializeAny(ParticleInfo itemInfo, String prefix, SchemaCollection schemaCollection) { boolean optional = XmlSchemaUtils.isParticleOptional(itemInfo.getParticle()) || (itemInfo.isArray() && itemInfo.getMinOccurs() == 0); boolean array = XmlSchemaUtils.isParticleArray(itemInfo.getParticle()); appendLine("var anyHolder = this._" + itemInfo.getJavascriptName() + ";"); appendLine("var anySerializer = null;"); appendLine("var anyXmlTag = null;"); appendLine("var anyXmlNsDef = null;"); appendLine("var anyData = null;"); appendLine("var anyStartTag;"); startIf("anyHolder != null && !anyHolder.raw"); appendLine("anySerializer = " + "cxfjsutils.interfaceObject.globalElementSerializers[anyHolder.qname];"); appendLine("anyXmlTag = '" + prefix + ":' + anyHolder.localName;"); appendLine("anyXmlNsDef = 'xmlns:" + prefix + "=\\'' + anyHolder.namespaceURI" + " + '\\'';"); appendLine("anyStartTag = '<' + anyXmlTag + ' ' + anyXmlNsDef + '>';"); appendLine("anyEndTag = '</' + anyXmlTag + '>';"); appendLine("anyEmptyTag = '<' + anyXmlTag + ' ' + anyXmlNsDef + '/>';"); appendLine("anyData = anyHolder.object;"); endBlock(); startIf("anyHolder != null && anyHolder.raw"); appendExpression("anyHolder.xml"); appendElse(); // first question: optional? if (optional) { startIf("anyHolder != null && anyData != null"); } else { startIf("anyHolder == null || anyData == null"); appendLine("throw 'null value for required any item';"); endBlock(); } String varRef = "anyData"; if (array) { startFor("var ax = 0", "ax < anyData.length", "ax ++"); varRef = "anyData[ax]"; // we need an extra level of 'nil' testing here. Or do we, depending // on the type structure? // Recode and fiddle appropriately. startIf(varRef + " == null"); appendExpression("anyEmptyTag"); appendElse(); } startIf("anySerializer"); // if no constructor, a simple type. // it has a value appendExpression("anySerializer.call(" + varRef + ", cxfjsutils, anyXmlTag, anyXmlNsDef)"); appendElse(); appendExpression("anyStartTag"); appendExpression("cxfjsutils.escapeXmlEntities(" + varRef + ")"); appendExpression("anyEndTag"); endBlock(); if (array) { endBlock(); endBlock(); } if (optional) { endBlock(); } endBlock(); // for raw } /** * If the object is an attribute or an anyAttribute, * return the 'Annotated'. If it's not one of those, or it's a group, * throw. We're not ready for groups yet. * @param object */ public static XmlSchemaAnnotated getObjectAnnotated(XmlSchemaObject object, QName contextName) { if (!(object instanceof XmlSchemaAnnotated)) { unsupportedConstruct("NON_ANNOTATED_ATTRIBUTE", object.getClass().getSimpleName(), contextName, object); } if (!(object instanceof XmlSchemaAttribute) && !(object instanceof XmlSchemaAnyAttribute)) { unsupportedConstruct("EXOTIC_ATTRIBUTE", object.getClass().getSimpleName(), contextName, object); } return (XmlSchemaAnnotated) object; } /** * If the object is an element or an any, return the particle. If it's not a particle, or it's a group, * throw. We're not ready for groups yet. * @param object */ public static XmlSchemaParticle getObjectParticle(XmlSchemaObject object, QName contextName, XmlSchema currentSchema) { if (!(object instanceof XmlSchemaParticle)) { unsupportedConstruct("NON_PARTICLE_CHILD", object.getClass().getSimpleName(), contextName, object); } if (object instanceof XmlSchemaGroupRef) { QName groupName = ((XmlSchemaGroupRef) object).getRefName(); XmlSchemaGroup group = currentSchema.getGroupByName(groupName); if (group == null) { unsupportedConstruct("MISSING_GROUP", groupName.toString(), contextName, null); } XmlSchemaParticle groupParticle = group.getParticle(); if (!(groupParticle instanceof XmlSchemaSequence)) { unsupportedConstruct("GROUP_REF_UNSUPPORTED_TYPE", groupParticle.getClass().getSimpleName(), contextName, groupParticle); } return groupParticle; } if (!(object instanceof XmlSchemaElement) && !(object instanceof XmlSchemaAny) && !(object instanceof XmlSchemaChoice) && !(object instanceof XmlSchemaSequence)) { unsupportedConstruct("GROUP_CHILD", object.getClass().getSimpleName(), contextName, object); } return (XmlSchemaParticle) object; } public static XmlSchemaSequence getSequence(XmlSchemaComplexType type) { XmlSchemaParticle particle = type.getParticle(); XmlSchemaSequence sequence = null; if (particle == null) { // the code that uses this wants to iterate. An empty one is more useful than // a null pointer, and certainly an exception. return EMPTY_SEQUENCE; } try { sequence = (XmlSchemaSequence) particle; } catch (ClassCastException cce) { unsupportedConstruct("NON_SEQUENCE_PARTICLE", type); } return sequence; } public static XmlSchemaChoice getChoice(XmlSchemaComplexType type) { XmlSchemaParticle particle = type.getParticle(); XmlSchemaChoice choice = null; if (particle == null) { // the code that uses this wants to iterate. An empty one is more useful than // a null pointer, and certainly an exception. return EMPTY_CHOICE; } try { choice = (XmlSchemaChoice) particle; } catch (ClassCastException cce) { unsupportedConstruct("NON_CHOICE_PARTICLE", type); } return choice; } public static XmlSchemaAll getAll(XmlSchemaComplexType type) { XmlSchemaParticle particle = type.getParticle(); XmlSchemaAll all = null; if (particle == null) { // the code that uses this wants to iterate. An empty one is more useful than // a null pointer, and certainly an exception. return EMPTY_ALL; } try { all = (XmlSchemaAll) particle; } catch (ClassCastException cce) { unsupportedConstruct("NON_CHOICE_PARTICLE", type); } return all; } public static List<XmlSchemaObject> getContentElements(XmlSchemaComplexType type, SchemaCollection collection) { List<XmlSchemaObject> results = new ArrayList<>(); QName baseTypeName = XmlSchemaUtils.getBaseType(type); if (baseTypeName != null) { XmlSchemaComplexType baseType = (XmlSchemaComplexType)collection.getTypeByQName(baseTypeName); // recurse onto the base type ... results.addAll(getContentElements(baseType, collection)); // and now process our sequence. XmlSchemaSequence extSequence = getContentSequence(type); if (extSequence != null) { for (XmlSchemaSequenceMember item : extSequence.getItems()) { /* * For now, leave the return type alone. Fix some day. */ results.add((XmlSchemaObject)item); } } return results; } else { // no base type, the simple case. XmlSchemaSequence sequence = getSequence(type); for (XmlSchemaSequenceMember item : sequence.getItems()) { results.add((XmlSchemaObject)item); } return results; } } public static XmlSchemaSequence getContentSequence(XmlSchemaComplexType type) { XmlSchemaContentModel model = type.getContentModel(); if (model == null) { return null; } XmlSchemaContent content = model.getContent(); if (content == null) { return null; } if (!(content instanceof XmlSchemaComplexContentExtension)) { return null; } XmlSchemaComplexContentExtension ext = (XmlSchemaComplexContentExtension)content; XmlSchemaParticle particle = ext.getParticle(); if (particle == null) { return null; } XmlSchemaSequence sequence = null; try { sequence = (XmlSchemaSequence) particle; } catch (ClassCastException cce) { unsupportedConstruct("NON_SEQUENCE_PARTICLE", type); } return sequence; } static void unsupportedConstruct(String messageKey, String what, QName subjectName, XmlSchemaObject subject) { Message message = new Message(messageKey, LOG, what, subjectName == null ? "anonymous" : subjectName, cleanedUpSchemaSource(subject)); throw new UnsupportedConstruct(message); } static void unsupportedConstruct(String messageKey, XmlSchemaType subject) { Message message = new Message(messageKey, LOG, subject.getQName(), cleanedUpSchemaSource(subject)); throw new UnsupportedConstruct(message); } static String cleanedUpSchemaSource(XmlSchemaObject subject) { if (subject == null || subject.getSourceURI() == null) { return ""; } else { return subject.getSourceURI() + ":" + subject.getLineNumber(); } } }