////////////////////////////////////////////////////////////////////////////////
//
// ADOBE SYSTEMS INCORPORATED
// Copyright 2003-2007 Adobe Systems Incorporated
// All Rights Reserved.
//
// NOTICE: Adobe permits you to use, modify, and distribute this file
// in accordance with the terms of the license agreement accompanying it.
//
////////////////////////////////////////////////////////////////////////////////
package mx.rpc.xml
{
import flash.utils.ByteArray;
import flash.utils.Dictionary;
import mx.collections.ArrayCollection;
import mx.utils.DescribeTypeCache;
import mx.utils.object_proxy;
import mx.utils.ObjectProxy;
import mx.utils.ObjectUtil;
import mx.collections.IList;
[ExcludeClass]
/**
* Encodes an ActionScript Object graph to XML based on an XML Schema.
*
* @private
*/
public class XMLEncoder extends SchemaProcessor implements IXMLEncoder
{
public function XMLEncoder()
{
super();
}
//--------------------------------------------------------------------------
//
// Methods
//
//--------------------------------------------------------------------------
/**
* Encodes an ActionScript value as XML.
*
* @param value The ActionScript value to encode as XML.
* @param name The QName of an XML Schema element that
* describes how to encode the value, or the name to be used for the
* encoded XML node when a type parameter is also specified.
* @param type The QName of an XML Schema simpleType or
* complexType definition that describes how to encode the
* @param definition If neither a top level element nor type exists in the
* schema to describe how to encode this value, a custom element definition
* can be provided.
*/
public function encode(value:*, name:QName = null, type:QName = null, definition:XML = null):XMLList
{
var result:XMLList = new XMLList();
var content:XML;
// FIXME: Handle generic name == null case with some default encoding?
if (name == null)
name = new QName("", "root");
if (type != null)
{
content = encodeXSINil(null, name, value);
if (content == null)
{
// If encodeXSINil didn't create content, we do now.
content = createElement(name);
// However, value can still be null if the element wasn't
// allowed to be nillable.
// FIXME: should we skip null or always create content with xsi:nil?
if (value == null)
setValue(content, null);
else
encodeType(type, content, name, value);
}
}
else
{
var elementDefinition:XML = definition;
var mustReleaseScope:Boolean = false;
if (elementDefinition == null)
{
elementDefinition = schemaManager.getNamedDefinition(name, constants.elementTypeQName);
// If we found a definition through the schemaManager, the relevant
// schema was pushed in scope, so we must remember to release it.
if (elementDefinition != null)
mustReleaseScope = true;
}
// Encoding is based on an element definition, either a custom one
// was provided or we looked one up for the given name. If no definition
// was found, encodeElementTopLevel will encode with anyType.
content = encodeElementTopLevel(elementDefinition, name, value);
if (mustReleaseScope)
schemaManager.releaseScope();
}
if (content != null)
result += content;
return result;
}
/**
* All content:
* (annotation?, (element | any)*)
*
* FIXME: This needs work, right now it treats all as a sequence.
* @private
*/
public function encodeAll(definition:XML, parent:XMLList, name:QName, value:*, isRequired:Boolean = true):Boolean
{
return encodeSequence(definition, parent, name, value, isRequired);
}
/**
* Encodes any complex object values as attributes using the XML schema
* rules for attribute wildcards.
*
* FIXME: This needs further investigation of the XML schema spec for
* wildcard rules and constraints.
*
* @private
*/
public function encodeAnyAttribute(definition:XML, parent:XML, name:QName, value:* = undefined, restriction:XML = null):void
{
// FIXME: honor restrictions for attributes
if (value !== undefined)
{
if (!isSimpleValue(value) && !(value is Array))
{
// FIXME: De we consider namespace filter attributes for
// encoding? Also consider skipping reserved attribute names
// like xmlns etc? What about non-public namespace properties?
for (var propertyName:Object in getProperties(value))
{
// Only add wildcard attributes if property has not already been added
if (!hasAttribute(value, propertyName) && !hasValue(value, propertyName))
{
var attributeValue:* = getAttribute(value, propertyName);
if (attributeValue != null)
setAttribute(parent, propertyName, attributeValue);
}
}
}
}
}
/**
* Encodes elements based on wildcard rules.
*
* Any content:
* (annotation?)
* @private
*
*/
public function encodeAnyElement(definition:XML, siblings:XMLList, name:QName, value:*, isRequired:Boolean=true, encodedVals:Dictionary=null):Boolean
{
// encodeAnyElement is never called with null value
// if (value == null)
// return false;
var maxOccurs:uint = getMaxOccurs(definition);
var minOccurs:uint = getMinOccurs(definition);
if (isSimpleValue(value))
{
var item:XML = createElement(name);
setValue(item, value);
appendValue(siblings, item);
}
else if (value is XML || value is XMLList)
{
// If we have XML or XMLList, just append it to the siblings list.
appendValue(siblings, value);
}
else
{
// We keep a dictionary of values we have encountered on this stack
// in order to detect cyclic references.
if (encodedVals == null)
encodedVals = new Dictionary(true);
// Work around AS problem with QName values in Dictionary. Since
// QNames cannot possibly create cyclic references, it's OK not to
// keep track of them.
if (!(value is QName))
{
if (encodedVals[value] != null)
throw new Error("Cannot encode complex structure. Cyclic references detected.");
encodedVals[value] = true;
}
if (value is Array || value is IList)
{
// FIXME: Check for maxOccurs and minOccurs.
if (value is IList)
value = IList(value).toArray();
for each (var arrValue:* in value as Array)
{
// Since this is an array, we don't need check for nillable
// or isRequired. We must always create a node to preserve
// array indexes.
var arrayItem:* = createElement(name);
if (arrValue == null)
{
// Directly set xsi:nil
setValue(arrayItem, null);
}
else if (arrValue != null)
{
var arrayChildren:XMLList = new XMLList();
encodeAnyElement(definition, arrayChildren, name, arrValue, isRequired, encodedVals);
if (isSimpleValue(arrValue))
{
// Don't double wrap simple values.
arrayItem = arrayChildren[0];
}
else
{
setValue(arrayItem, arrayChildren);
}
}
appendValue(siblings, arrayItem);
}
}
else
{
for each (var objProperty:Object in getProperties(value))
{
var propValue:* = getValue(value, objProperty);
var propQName:QName = new QName(name.uri, objProperty);
// Only encode if property hasn't been encoded yet.
// FIXME: When coming from a group context, if the any element
// is not last in the definition, we will end up encoding subsequent
// named elements twice. We need all sibling names from the definition
// of the group to properly do this.
if (!containsNodeByName(siblings, propQName))
{
var propItem:XML = encodeXSINil(definition, propQName, propValue);
if (propItem != null)
{
appendValue(siblings, propItem);
}
else if (propValue != null)
{
var propChildren:XMLList = new XMLList();
encodeAnyElement(definition, propChildren, propQName, propValue, isRequired, encodedVals);
appendValue(siblings, propChildren);
}
}
}
}
delete encodedVals[value];
}
// FIXME: figure out isRequired
return true;
}
/**
* An attribute must be based on a simple type and thus will have simple
* content encoded as a String.
*
* This function is used to encode an attribute that may be
* named and registered as a top-level schema definition or
* in-line from a complexType, extension or
* of either a complexType or
* simpleType, or attributeGroup
* definition in any aforementioned parent component.
*
* If the attribute points to a named definition using a
* ref attribute, the reference is resolved to provide the
* real definition of the attribute. If the reference cannot be resolved,
* an error is thrown.
*
* If the attribute defines a fixed constraint then any value
* provided is ignored and the fixed value is used instead. If a value is
* not provided and the attribute defines a default, the
* default is used for the encoded attribute. Otherwise if an attribute is
* marked as optional and a value is not provided it will be
* skipped.
*
* @param parent The parent instance to which these attributes will be added.
* @param definition The XML schema definition of the attribute.
* @param value An object with a property name that matches the resolved
* attribute name. The property value will be used as the encoded attribute
* value.
*
* FIXME: Attributes are expected to be simple values and must be ultimately
* representable as a String. If a complex value is passed to this method
* should we assume that we're always looking for a property with the same
* name as the attribute? We may need to because if we have a ref then the
* name is not known immediately...
*
* @private
*/
public function encodeAttribute(definition:XML, parent:XML, name:QName, value:* = undefined, restriction:XML = null):void
{
// may be used to point to a top-level attribute definition
var ref:QName;
if (definition.attribute("ref").length() == 1)
{
ref = schemaManager.getQNameForPrefixedName(definition.@ref, definition, true);
definition = schemaManager.getNamedDefinition(ref, constants.attributeQName);
if (definition == null)
throw new Error("Cannot resolve attribute definition for '" + ref + "'");
}
// FIXME: Check restriction for prohibited attribute definitions too
var attributeNameString:String = definition.@name.toString();
var attributeUse:String = definition.attribute("use").toString();
if (attributeUse != "prohibited")
{
var attributeName:QName = schemaManager.getQNameForAttribute(attributeNameString, getAttributeFromNode("form", definition));
var attributeFixed:String = getAttributeFromNode("fixed", definition);
if (attributeFixed != null)
{
value = attributeFixed;
}
else
{
value = getValue(value, attributeName);
if (value === undefined)
{
var attributeDefault:String = getAttributeFromNode("default", definition);
if (attributeDefault != null)
value = attributeDefault;
}
}
var tempElement:*;
var attributeFound:Boolean;
if (value !== undefined)
{
var typeDefinition:XML;
// we just need a temporary wrapper to pass down as
// the parent XML for the encodeSimpleType calls
tempElement = ;
// An may declare a type="QName" attribute which
// refers to either a built-in schema type or a previously
// declared
var typeName:String = getAttributeFromNode("type", definition);
var attributeType:QName;
if (typeName != null)
attributeType = schemaManager.getQNameForPrefixedName(definition.@type, definition);
else
attributeType = schemaManager.schemaDatatypes.anySimpleTypeQName;
if (attributeType != null)
{
if (isBuiltInType(attributeType))
{
tempElement.appendChild(schemaManager.marshall(value, attributeType, restriction));
}
else
{
//
typeDefinition = schemaManager.getNamedDefinition(attributeType, constants.simpleTypeQName);
if (typeDefinition != null)
encodeSimpleType(typeDefinition, tempElement, attributeName, value, restriction);
else
throw new Error("Cannot find simpleType " + attributeType + " for attribute " + attributeName);
// Then release the scope after we've found the attribute type
schemaManager.releaseScope();
}
}
else
{
// Otherwise, an may define a single anonymous
// child in-line
typeDefinition = getSingleElementFromNode(definition, constants.simpleTypeQName);
if (typeDefinition != null)
{
encodeSimpleType(typeDefinition, tempElement, attributeName, value, restriction);
}
else if (value != null)
{
// Finally, in the absence of type information we
// just get the attribute value as a String without
// restriction
tempElement.appendChild(value.toString());
}
}
}
// FIXME: Should we enforce use="required"?
if (tempElement !== undefined)
{
setAttribute(parent, attributeName, tempElement);
}
}
// If we found our attribute by reference, we now release the schema scope
if (ref != null)
schemaManager.releaseScope();
}
/**
* An attributeGroup definition may include a number of
* attribute or attributeGroup children, all of
* which ultimately combine to form a flat group of attributes for some
* type. It may also specify anyAttribute which expands
* the definition to accept attributes based on more general criteria
* (such excluding or including attributes on namespace).
*
* This function is used to encode an attributeGroup that may
* be named and registered as a top-level schema definition or
* in-line from a complexType, extension or
* of either a complexType or
* simpleType, or even another attributeGroup
* definition in any aforementioned parent component.
*
* If the attributeGroup points to a named definition using a
* ref attribute, the reference is resolved to provide the real definition
* of the attributeGroup. If the reference cannot be resolved, an error is
* thrown.
*
* @param parent The parent instance to which these attributes will be added.
* @param definition The XML schema definition of the attributeGroup.
* @param value An object with property names that match the resolved
* attribute names in the group. The property values will be used as the
* encoded attribute values. This argument may be omitted if each attribute
* in the group has a fixed or default value.
*
* @private
*/
public function encodeAttributeGroup(definition:XML, parent:XML, name:QName, value:* = undefined, restriction:XML = null):void
{
// may be used to point to a top-level
// attributeGroup definition which must first be resolved.
var ref:QName;
if (definition.attribute("ref").length() == 1)
{
ref = schemaManager.getQNameForPrefixedName(definition.@ref, definition, true);
definition = schemaManager.getNamedDefinition(ref, constants.attributeGroupQName);
if (definition == null)
throw new Error("Cannot resolve attributeGroup definition for '" + ref + "'");
}
//
var attributes:XMLList = definition.elements(constants.attributeQName);
for each (var attributeDefinition:XML in attributes)
{
encodeAttribute(attributeDefinition, parent, name, value, restriction);
}
//
var attributeGroups:XMLList = definition.elements(constants.attributeGroupQName);
for each (var attributeGroup:XML in attributeGroups)
{
encodeAttributeGroup(attributeGroup, parent, name, value, restriction);
}
//
var anyAttribute:XML = getSingleElementFromNode(definition, constants.anyAttributeQName);
if (anyAttribute != null)
{
encodeAnyAttribute(anyAttribute, parent, name, value, restriction);
}
// If we found our attributeGroup by reference, we now release the schema scope
if (ref != null)
schemaManager.releaseScope();
}
/**
* choice:
* (annotation?, (element | group | choice | sequence | any)*)
*
* @private
*/
public function encodeChoice(definition:XML, parent:XMLList, name:QName, value:*, isRequired:Boolean = true):Boolean
{
var maxOccurs:uint = getMaxOccurs(definition);
var minOccurs:uint = getMinOccurs(definition);
// If maxOccurs is 0 this choice must not be present.
// If minOccurs == 0 the choice is optional so it can be omitted if
// a value was not provided.
if (maxOccurs == 0)
return false;
if (value == null && minOccurs == 0)
return true;
var choiceElements:XMLList = definition.elements();
var choiceSatisfied:Boolean = true;
var lastIndex:uint;
var choiceOccurs:uint;
// We don't enforce occurs bounds on the choice element itself. Since all
// child elements of the choice definition would be properties on the
// value object, simply looping through the choice children once would
// encode the values that apply to each of the child elements.
// An empty choice is satisfied by default, but if there are choiceElements
// we need to start out with choiceSatisfied = false
if (choiceElements.length() > 0)
choiceSatisfied = false;
for each (var childDefinition:XML in choiceElements)
{
if (childDefinition.name() == constants.elementTypeQName)
{
//
choiceSatisfied = encodeGroupElement(childDefinition, parent,
name, value, false) || choiceSatisfied;
}
else if (childDefinition.name() == constants.sequenceQName)
{
//
choiceSatisfied = encodeSequence(childDefinition, parent,
name, value, false) || choiceSatisfied;
}
else if (childDefinition.name() == constants.groupQName)
{
//
choiceSatisfied = encodeGroupReference(childDefinition, parent,
name, value, false) || choiceSatisfied;
}
else if (childDefinition.name() == constants.choiceQName)
{
//
choiceSatisfied = encodeChoice(childDefinition, parent,
name, value, false) || choiceSatisfied;
}
else if (childDefinition.name() == constants.anyQName)
{
//
choiceSatisfied = encodeAnyElement(childDefinition, parent,
name, value, false) || choiceSatisfied;
}
}
return choiceSatisfied;
}
/**
* Derivation by restriction takes an existing type as the base and creates
* a new type by limiting its allowed content to a subset of that allowed
* by the base type. Derivation by extension takes an existing type as the
* base and creates a new type by adding to its allowed content.
*
* complexContent:
* (annotation?, (restriction | extension))
*
* @private
*/
public function encodeComplexContent(definition:XML, parent:XML, name:QName, value:*):void
{
var childDefinition:XML = getSingleElementFromNode(definition, constants.extensionQName, constants.restrictionQName);
if (childDefinition.name() == constants.extensionQName)
{
encodeComplexExtension(childDefinition, parent, name, value);
}
else if (childDefinition.name() == constants.restrictionQName)
{
encodeComplexRestriction(childDefinition, parent, name, value);
}
}
/**
* complexContent:
* extension:
* (annotation?, ((group | all | choice | sequence)?, ((attribute | attributeGroup)*, anyAttribute?), (assert | report)*))
*
* @private
*/
public function encodeComplexExtension(definition:XML, parent:XML, name:QName, value:*):void
{
var baseName:String = getAttributeFromNode("base", definition);
if (baseName == null)
throw new Error ("A complexContent extension must declare a base type.");
var baseType:QName = schemaManager.getQNameForPrefixedName(baseName, definition);
// complexContent base type must be a complexType
var baseDefinition:XML = schemaManager.getNamedDefinition(baseType, constants.complexTypeQName);
if (baseDefinition == null)
throw new Error("Cannot find base type definition '" + baseType + "'");
// FIXME: Should we care if base type is marked final?
// First encode all of the properties of the base type
encodeComplexType(baseDefinition, parent, name, value);
// Then release the scope of the base type definition
schemaManager.releaseScope();
var childDefinitions:XMLList = definition.elements();
// Start a separate XMLList for the child elements defined in this extension.
// Extension attributes are still encoded directly on the parent.
var extChildren:XMLList = new XMLList();
for each (var childDefinition:XML in childDefinitions)
{
if (childDefinition.name() == constants.sequenceQName)
{
//
encodeSequence(childDefinition, extChildren, name, value);
}
else if (childDefinition.name() == constants.groupQName)
{
//
encodeGroupReference(childDefinition, extChildren, name, value);
}
else if (childDefinition.name() == constants.allQName)
{
//
encodeAll(childDefinition, extChildren, name, value);
}
else if (childDefinition.name() == constants.choiceQName)
{
//
encodeChoice(childDefinition, extChildren, name, value);
}
else if (childDefinition.name() == constants.attributeQName)
{
//
encodeAttribute(childDefinition, parent, name, value);
}
else if (childDefinition.name() == constants.attributeGroupQName)
{
//
encodeAttributeGroup(childDefinition, parent, name, value);
}
else if (childDefinition.name() == constants.anyAttributeQName)
{
//
encodeAnyAttribute(childDefinition, parent, name, value);
}
}
// We need to add the extension elements to the parent node. However,
// we need to handle the case where a value fits both the base and the
// extension definitions (strictly speaking that's illegal schema, but
// it's used in some cases). We need to keep the values encoded with the
// extension definition, so we delete any values with the same names that
// we got from encoding the base definition.
for each (var extension:XML in extChildren)
{
// Delete anything already encoded during base type processing, which
// matches the full qualified name of this extension element.
delete parent[extension.name()];
// Also delete unqualified elements with the same local name, since
// in the base definition would encode with local names only.
delete parent[new QName("", extension.name().localName)];
delete parent[new QName(null, extension.name().localName)];
}
setValue(parent, extChildren);
}
/**
* complexContent:
* restriction:
* (annotation?, (group | all | choice | sequence)?, ((attribute | attributeGroup)*, anyAttribute?), (assert | report)*)
*
* @private
*/
public function encodeComplexRestriction(restriction:XML, parent:XML, name:QName, value:*):void
{
var baseName:String = getAttributeFromNode("base", restriction);
if (baseName == null)
throw new Error ("A complexContent restriction must declare a base type.");
var baseType:QName = schemaManager.getQNameForPrefixedName(baseName, restriction);
// FIXME: Validate complex restriction based on the base type definition
// complexContent base type must be a complexType
// var baseDefinition:XML = schemaManager.getNamedDefinition(baseType, constants.complexTypeQName);
// if (baseDefinition == null)
// throw new Error("Cannot find base type definition '" + baseType + "'");
// FIXME: Should we care if base type is marked final?
var childDefinitions:XMLList = restriction.elements();
var children:XMLList = parent.elements();
for each (var childDefinition:XML in childDefinitions)
{
if (childDefinition.name() == constants.sequenceQName)
{
//
encodeSequence(childDefinition, children, name, value);
}
else if (childDefinition.name() == constants.groupQName)
{
//
encodeGroupReference(childDefinition, children, name, value);
}
else if (childDefinition.name() == constants.allQName)
{
//
encodeAll(childDefinition, children, name, value);
}
else if (childDefinition.name() == constants.choiceQName)
{
//
encodeChoice(childDefinition, children, name, value);
}
else if (childDefinition.name() == constants.attributeQName)
{
//
encodeAttribute(childDefinition, parent, name, value, restriction);
}
else if (childDefinition.name() == constants.attributeGroupQName)
{
//
encodeAttributeGroup(childDefinition, parent, name, value, restriction);
}
else if (childDefinition.name() == constants.anyAttributeQName)
{
//
encodeAnyAttribute(childDefinition, parent, name, value, restriction);
}
}
parent.setChildren(children);
}
public function encodeComplexType(definition:XML, parent:XML, name:QName, value:*, restriction:XML = null):void
{
var childElements:XMLList = definition.elements();
var children:XMLList = new XMLList();
// FIXME: Investigate if we need to support "base" attribute on
// complexType as short-cut as seen in some examples...
for each (var childDefinition:XML in childElements)
{
if (childDefinition.name() == constants.sequenceQName)
{
//
encodeSequence(childDefinition, children, name, value);
}
else if (childDefinition.name() == constants.simpleContentQName)
{
//
encodeSimpleContent(childDefinition, parent, name, value, restriction);
}
else if (childDefinition.name() == constants.complexContentQName)
{
//
encodeComplexContent(childDefinition, parent, name, value);
}
else if (childDefinition.name() == constants.groupQName)
{
//
encodeGroupReference(childDefinition, children, name, value);
}
else if (childDefinition.name() == constants.allQName)
{
//
encodeAll(childDefinition, children, name, value);
}
else if (childDefinition.name() == constants.choiceQName)
{
//
encodeChoice(childDefinition, children, name, value);
}
else if (childDefinition.name() == constants.attributeQName)
{
//
encodeAttribute(childDefinition, parent, name, value, restriction);
}
else if (childDefinition.name() == constants.attributeGroupQName)
{
//
encodeAttributeGroup(childDefinition, parent, name, value, restriction);
}
else if (childDefinition.name() == constants.anyAttributeQName)
{
//
encodeAnyAttribute(childDefinition, parent, name, value, restriction);
}
}
setValue(parent, children);
}
/**
* Used to encode a local element definition (inside a model group).
* Handles restrictions on omittability and occurence counts in the
* context of the parent model group.
* Delegates actual encoding to encodeElementTopLevel once all the
* context around the element is known.
*
* @param definition The XML Schema definition of the local element.
* @param parent The XMLList of values encoded in the current level. The
* new encoded node should be appended to this XMLList.
* @param name The QName to be used for the encoded XML node.
* @param value The ActionScript value to encode as XML.
* @param isRequired A flag indicating wether the element should meet
* its local occurence bounds. For example, the local element may have
* minOccurs=1, but be only one of many elements in a choice group, in
* which case it is valid not to satisfy the minOccurs requirement.
*
* @return Wether or not the value provided
*
* FIXME: Support substitutionGroup, block and redefine?
* FIXME: Do we care about abstract or final?
* @private
*/
public function encodeGroupElement(definition:XML, siblings:XMLList, name:QName, value:*, isRequired:Boolean = true):Boolean
{
// occur on the local element,
// not on a referent, so we capture this information first.
var maxOccurs:uint = getMaxOccurs(definition);
var minOccurs:uint = getMinOccurs(definition);
isRequired = isRequired && minOccurs > 0;
// BugFix: moved this check before the Ref: lookup that can balls-up the schema stack.
// If the maximum occurence is 0 this element must not be present.
if (maxOccurs == 0)
return true;
// may be used to point to a top-level element definition
var ref:QName;
if (definition.attribute("ref").length() == 1)
{
ref = schemaManager.getQNameForPrefixedName(definition.@ref, definition, true);
definition = schemaManager.getNamedDefinition(ref, constants.elementTypeQName);
if (definition == null)
throw new Error("Cannot resolve element definition for ref '" + ref + "'");
}
var elementName:String = definition.@name.toString();
var elementQName:QName = schemaManager.getQNameForElement(elementName, getAttributeFromNode("form", definition));
// Now that we've resolved the real element name, look for the element's
// value on the provided value.
var elementValue:* = getValue(value, elementQName);
var encodedElement:XML;
// If minOccurs == 0 the element is optional so we can omit it if
// a value was not provided.
if (elementValue == null)
{
encodedElement = encodeElementTopLevel(definition, elementQName, elementValue);
if (encodedElement != null)
appendValue(siblings, encodedElement);
//Bugfix : nodes when enoded with missing values were causing the schema stack to parf out resulting in much pain!
if (ref != null)
schemaManager.releaseScope();
// if required, but no value was encoded, the definition is not
// satisfied
if (isRequired && encodedElement == null)
return false;
// Otherwise we can return true
return true;
}
// We treat maxOccurs="1" as a special case and not check the
// occurence because we need to pass through values to SOAP
// encoded Arrays which do not rely on minOccurs/maxOccurs
if (maxOccurs == 1)
{
encodedElement = encodeElementTopLevel(definition, elementQName, elementValue);
if (encodedElement != null)
{
appendValue(siblings, encodedElement);
}
// ...else we just skip the element as a value wasn't provided.
}
else if (maxOccurs > 1)
{
var valueOccurence:uint = getValueOccurence(elementValue);
// If maxOccurs is greater than 1 then we would expect an
// Array of values
if (valueOccurence < minOccurs)
{
throw new Error("Value supplied for element '" + elementQName +
"' occurs " + valueOccurence + " times which falls short of minOccurs " +
minOccurs + ".");
}
if (valueOccurence > maxOccurs)
{
throw new Error("Value supplied for element of type '" + elementQName +
"' occurs " + valueOccurence + " times which exceeds maxOccurs " +
maxOccurs + ".");
}
// Promote non-iterable values to an Array to handle the MXML
// single-child property case where the compiler doesn't promote
// a property to an Array until two items are present.
if (!TypeIterator.isIterable(elementValue))
elementValue = [elementValue];
// Encode element based on occurence within the bounds of
// minOccurs and maxOccurs
var iter:TypeIterator = new TypeIterator(elementValue);
for (var i:uint = 0; i < maxOccurs && i < valueOccurence; i++)
{
var item:*;
if (iter.hasNext())
{
item = iter.next();
}
else if (i > minOccurs)
{
break;
}
encodedElement = encodeElementTopLevel(definition, elementQName, item);
// encodedElement is null if encodeXSINil inside encodeElementTopLevel
// was not allowed to create element with xsi:nil for a null or undefined
// value. We must still force xsi:nil, because we are encoding an array
// and we need to preserve the index.
if (encodedElement == null)
{
encodedElement = createElement(elementQName);
setValue(encodedElement, null);
}
appendValue(siblings, encodedElement);
}
}
// If we found our element by reference, we now release the schema scope
if (ref != null)
schemaManager.releaseScope();
return true;
}
/**
* Element content:
* (annotation?, ((simpleType | complexType)?, (unique | key | keyref)*))
*
* @private
*/
public function encodeElementTopLevel(definition:XML, elementQName:QName, value:*):XML
{
// Let encodeXSINil create an element if null, fixed, or default value
// must be used.
var element:XML = encodeXSINil(definition, elementQName, value);
// If ecnodeXSINil created an element, we are done.
if (element != null)
return element;
// If value was null, but element wasn't created, it must be omitted.
else if (value == null)
return null;
// Otherwise, just create the element with the given QName. This starts off
// a new tree of encoded values with this top level element as the root.
element = createElement(elementQName);
// Check for a simple def first, falling back to complex type handling
// and then default handling.
var typeAttribute:String = getAttributeFromNode("type", definition);
if (typeAttribute != null)
{
var typeQName:QName = schemaManager.getQNameForPrefixedName(typeAttribute, definition);
encodeType(typeQName, element, elementQName, value);
}
// Next, check if the element has an in-line or
// definition.
else if (definition != null && definition.hasComplexContent())
{
var typeDefinition:XML = getSingleElementFromNode(definition,
constants.complexTypeQName,
constants.simpleTypeQName);
if (typeDefinition.name() == constants.complexTypeQName)
{
//
encodeComplexType(typeDefinition, element, elementQName, value);
}
else if (typeDefinition.name() == constants.simpleTypeQName)
{
//
encodeSimpleType(typeDefinition, element, elementQName, value);
}
// FIXME: Support unique, key, keyref, field, selector
}
else
{
// FIXME: Support
encodeType(constants.anyTypeQName, element, elementQName, value);
}
return element;
}
/**
* The group element allows partial (or complete) content
* models to be reused in complex types. When used inside a choice,
* sequence, complexType, extension or restriction element, it must
* have a ref attribute, specifying the name of a global definition
* of a named model group.
*
* group:
* (annotation?, (all | choice | sequence)?)
*
* @private
*/
public function encodeGroupReference(definition:XML, parent:XMLList, name:QName, value:*, isRequired:Boolean = true):Boolean
{
// must be used to point to a top-level group definition
var ref:QName;
if (definition.attribute("ref").length() == 1)
{
ref = schemaManager.getQNameForPrefixedName(definition.@ref, definition, true);
definition = schemaManager.getNamedDefinition(ref, constants.groupQName);
if (definition == null)
throw new Error("Cannot resolve group definition for '" + ref + "'");
}
else
{
throw new Error("A group reference element must have the ref attribute");
}
var groupElements:XMLList = definition.elements();
var groupSatisfied:Boolean = false;
for each (var childDefinition:XML in groupElements)
{
if (childDefinition.name() == constants.sequenceQName)
{
//
groupSatisfied = encodeSequence(childDefinition, parent, name, value, isRequired);
}
else if (childDefinition.name() == constants.allQName)
{
//
groupSatisfied = encodeAll(childDefinition, parent, name, value, isRequired);
}
else if (childDefinition.name() == constants.choiceQName)
{
//
groupSatisfied = encodeChoice(childDefinition, parent, name, value, isRequired);
}
}
// We found our group by reference, we now release the schema scope
schemaManager.releaseScope();
return groupSatisfied;
}
/**
* sequence:
* (annotation?, (element | group | choice | sequence | any)*)
*
* @private
*/
public function encodeSequence(definition:XML, siblings:XMLList, name:QName, value:*, isRequired:Boolean=true):Boolean
{
var maxOccurs:uint = getMaxOccurs(definition);
var minOccurs:uint = getMinOccurs(definition);
// If maxOccurs is 0 this sequence must not be present.
// If minOccurs == 0 the sequence is optional so it can be omitted if
// a value was not provided.
if (maxOccurs == 0)
return true;
if (value == null && minOccurs == 0)
return true;
// Note that we can't enforce occurence count on the sequence element
// itself. Since the value is an ActionScript object, any named element
// in the sequence should correspond to a named property on the object.
var sequenceElements:XMLList = definition.elements();
// We loop through the children of the sequence definition. We require
// all child definitions to be satisfied, unless the sequence itself
// doesn't need to be satisfied.
var requireChild:Boolean = isRequired && minOccurs > 0;
var sequenceSatisfied:Boolean = true;
//Original behaviour if maxOccurs = 1
if (maxOccurs == 1)
{
for each (var childDefinition:XML in sequenceElements)
{
sequenceSatisfied = false;
if (childDefinition.name() == constants.elementTypeQName)
{
//
if (!encodeGroupElement(childDefinition, siblings, name, value, isRequired))
break;
}
else if (childDefinition.name() == constants.groupQName)
{
//
if (!encodeGroupReference(childDefinition, siblings, name, value, isRequired))
break;
}
else if (childDefinition.name() == constants.choiceQName)
{
//
if (!encodeChoice(childDefinition, siblings, name, value, isRequired))
break;
}
else if (childDefinition.name() == constants.sequenceQName)
{
//
if (!encodeSequence(childDefinition, siblings, name, value, isRequired))
break;
}
else if (childDefinition.name() == constants.anyQName)
{
//
if (!encodeAnyElement(childDefinition, siblings, name, value, isRequired))
break;
}
sequenceSatisfied = true;
}
}
else if (maxOccurs > 1)
{
//We're making assumptions that value is iterable
for each (var childValue:* in value)
{
for each (childDefinition in sequenceElements)
{
sequenceSatisfied = false;
if (childDefinition.name() == constants.elementTypeQName)
{
//
if (!encodeGroupElement(childDefinition, siblings, name, childValue, isRequired))
break;
}
else if (childDefinition.name() == constants.groupQName)
{
//
if (!encodeGroupReference(childDefinition, siblings, name, childValue, isRequired))
break;
}
else if (childDefinition.name() == constants.choiceQName)
{
//
if (!encodeChoice(childDefinition, siblings, name, childValue, isRequired))
break;
}
else if (childDefinition.name() == constants.sequenceQName)
{
//
if (!encodeSequence(childDefinition, siblings, name, childValue, isRequired))
break;
}
else if (childDefinition.name() == constants.anyQName)
{
//
if (!encodeAnyElement(childDefinition, siblings, name, childValue, isRequired))
break;
}
sequenceSatisfied = true;
}
}
}
return sequenceSatisfied || !isRequired;
}
/**
* simpleContent specifies that the content will be simple text
* only, that is it conforms to a simple type and will not contain elements,
* although it may also define attributes.
*
* A simpleContent must be defined with an extension or a restriction. An
* extension specifies the attribute definitions that are to be added to the
* type and the base attribute specifies from which simple data type this
* custom type is defined. A restriction for simpleContent is less common,
* although it may be used to prohibit attributes in derived types also
* with simpleContent.
*
* simpleContent
* (annotation?, (restriction | extension))
*
* @private
*/
public function encodeSimpleContent(definition:XML, parent:XML, name:QName, value:*, restriction:XML = null):void
{
var childDefinition:XML = getSingleElementFromNode(definition, constants.extensionQName, constants.restrictionQName);
if (childDefinition != null)
{
var baseName:String = getAttributeFromNode("base", childDefinition);
if (baseName == null)
throw new Error ("A simpleContent extension or restriction must declare a base type.");
var baseType:QName = schemaManager.getQNameForPrefixedName(baseName, childDefinition);
if (!isBuiltInType(baseType))
{
var baseDefinition:XML = schemaManager.getNamedDefinition(baseType,
constants.complexTypeQName, constants.simpleTypeQName);
if (baseDefinition == null)
throw new Error("Cannot find base type definition '" + baseType + "'");
// We found our baseType by name so we now release the schema scope
schemaManager.releaseScope();
}
// FIXME: Should we care if base type is marked final?
var simpleValue:*;
//
if (childDefinition.name() == constants.extensionQName)
{
// simpleContent base type must be a simpleType or a complexType
// that ultimately has simpleContent (FIXME: we currently don't verify the latter)
if (isBuiltInType(baseType))
{
simpleValue = getSimpleValue(value, name);
setValue(parent, schemaManager.marshall(simpleValue, baseType, restriction));
}
else
{
encodeType(baseType, parent, value, restriction);
}
var extensions:XMLList = childDefinition.elements();
for each (var extensionChild:XML in extensions)
{
if (extensionChild.name() == constants.attributeQName)
{
//
encodeAttribute(extensionChild, parent, name, value, restriction);
}
else if (extensionChild.name() == constants.attributeGroupQName)
{
//
encodeAttributeGroup(extensionChild, parent, name, value, restriction);
}
else if (extensionChild.name() == constants.anyAttributeQName)
{
//
encodeAnyAttribute(extensionChild, parent, name, value, restriction);
}
}
}
//
else if (childDefinition.name() == constants.restrictionQName)
{
simpleValue = getSimpleValue(value, name);
encodeSimpleRestriction(childDefinition, parent, name, simpleValue);
}
}
}
/**
* A simpleType may declare a list of space separated
* simple content for a single value.
*
*
* Content: (annotation?, simpleType?)
*
*
* @private
*/
public function encodeSimpleList(definition:XML, parent:XML, name:QName, value:*, restriction:XML = null):void
{
var itemTypeAttribute:String = definition.@itemType;
var itemTypeQName:QName;
var itemDefinition:XML;
// The simple type of each item in the list can be specified by
// either an itemType attribute, or a simpleType inline definition.
if (itemTypeAttribute != "")
itemTypeQName = schemaManager.getQNameForPrefixedName(itemTypeAttribute, definition);
else
itemDefinition = getSingleElementFromNode(definition, constants.simpleTypeQName);
var listValue:String = "";
if (!TypeIterator.isIterable(value))
value = [value];
var iter:TypeIterator = new TypeIterator(value);
while (iter.hasNext())
{
var item:* = iter.next();
var tempElement:* = ;
// Lists cannot encode null values, since separators are collapsed.
if (item == null)
continue;
if (itemTypeQName != null)
encodeType(itemTypeQName, tempElement, name, item, restriction);
else
encodeSimpleType(itemDefinition, tempElement, name, item, restriction);
listValue = listValue.concat(tempElement.toString());
if (iter.hasNext())
listValue = listValue.concat(" ");
}
setValue(parent, listValue);
}
/**
* simpleType:
* restriction: (annotation?, (simpleType?,
* (minExclusive | minInclusive | maxExclusive | maxInclusive |
* totalDigits | fractionDigits | maxScale | minScale | length |
* minLength | maxLength | enumeration | whiteSpace | pattern)*))
*
* @private
*/
public function encodeSimpleRestriction(restriction:XML, parent:XML, name:QName, value:*):void
{
var simpleTypeDefinition:XML = getSingleElementFromNode(restriction, constants.simpleTypeQName);
if (simpleTypeDefinition != null)
{
encodeSimpleType(simpleTypeDefinition, parent, name, value, restriction);
}
else
{
var baseName:String = getAttributeFromNode("base", restriction);
var baseType:QName = schemaManager.getQNameForPrefixedName(baseName, restriction);
// FIXME: handle anyType
encodeType(baseType, parent, name, value, restriction);
}
}
/**
*
* Content: (annotation?, (restriction | list | union))
*
*
* @private
*/
public function encodeSimpleType(definition:XML, parent:XML, name:QName, value:*, restriction:XML = null):void
{
var definitionChild:XML = getSingleElementFromNode(definition,
constants.restrictionQName,
constants.listQName,
constants.unionQName);
if (definitionChild.name() == constants.restrictionQName)
{
//
encodeSimpleRestriction(definitionChild, parent, name, value);
}
else if (definitionChild.name() == constants.listQName)
{
//
encodeSimpleList(definitionChild, parent, name, value, restriction);
}
else if (definitionChild.name() == constants.listQName)
{
//
encodeSimpleUnion(definitionChild, parent, name, value, restriction);
}
}
/**
*
* Content: (annotation?, simpleType*)
*
*
* FIXME: This needs a lot of work.
*
* @private
*/
public function encodeSimpleUnion(definition:XML, parent:XML, name:QName, value:*, restriction:XML = null):void
{
var memberList:String = getAttributeFromNode("memberTypes", definition);
var memberArray:Array = memberList.split(" ");
var type:QName;
var args:*;
//as the memeber types can contain simple data types like xsd:string or tns:address
for (var i:int = 0; i < memberArray.length; i++)
{
var prefixedName:String = memberArray[i];
var simpleType:QName = schemaManager.getQNameForPrefixedName(prefixedName, definition);
if (!isBuiltInType(simpleType))
{
args = getValue(value, simpleType);
if (args !== undefined)
{
type = simpleType;
break;
}
}
}
if (!type)
{
type = schemaManager.schemaDatatypes.stringQName;
}
setValue(parent, schemaManager.marshall(value, type, restriction));
}
/**
* Allow instance specific overrides for concrete type information as
* abstract complexTypes may require a concrete xsi:type definition.
*
* @param parent A reference to the parent XML. Must not be null.
*
* @private
*/
public function encodeType(type:QName, parent:XML, name:QName, value:*, restriction:XML = null):void
{
var xsiType:QName = getXSIType(value);
if (xsiType != null)
type = xsiType;
var definition:XML = schemaManager.getNamedDefinition(type,
constants.complexTypeQName, constants.simpleTypeQName);
if (isBuiltInType(type))
{
if (type == constants.anyTypeQName && !isSimpleValue(value))
{
var children:XMLList = new XMLList();
encodeAnyElement(definition, children, name, value);
setValue(parent, children);
}
else
{
setValue(parent, schemaManager.marshall(value, type, restriction));
}
deriveXSIType(parent, type, value);
}
else
{
if (definition == null)
throw new Error("Cannot find definition for type '" + type + "'");
var definitionType:QName = definition.name() as QName;
if (definitionType == constants.complexTypeQName)
{
//
encodeComplexType(definition, parent, name, value, restriction);
}
else if (definitionType == constants.simpleTypeQName)
{
//
encodeSimpleType(definition, parent, name, value, restriction);
}
else
{
throw new Error("Invalid type definition " + definitionType);
}
}
// If we found our type definition by name we release the schema scope.
if (definition != null)
schemaManager.releaseScope();
}
/**
* Sets the xsi:nil attribute when necessary
*
* @param definition The Schema definition of the expected type. If
* nillable is strictly enforced, this definition must explicitly
* specify nillable=true.
*
* @param name The name of the element to be created
*
* @param value The value to check
*
* @return content The element where xsi:nil was set, or null if xsi:nil was
* not set.
*/
public function encodeXSINil(definition:XML, name:QName, value:*, isRequired:Boolean = true):XML
{
var strictNillability:Boolean = false;
// Check for nillable in the definition only if strictNillability is true.
// Otherwise assume nillable=true.
var nillable:Boolean = true;
if (strictNillability)
{
if (definition != null)
nillable = definition.@nillable.toString() == "true" ? true : false;
else
nillable = false; //XML schema default for nillable
}
var item:XML;
//
// Fixed is forbidden when nillable="true". We enforce that only if
// strictNillability==true. Otherwise we take the fixed value if it
// is provided.
var fixedValue:String = getAttributeFromNode("fixed", definition);
if (!(strictNillability && nillable) && fixedValue != null)
{
item = createElement(name);
setValue(item, schemaManager.marshall(fixedValue, schemaManager.schemaDatatypes.stringQName));
return item;
}
// After we are done with fixed, which can replace even a non-null value,
// we only care about cases where value is null, so we can return otherwise.
if (value != null)
return null;
//
var defaultValue:String = getAttributeFromNode("default", definition);
if (value == null && defaultValue != null)
{
item = createElement(name);
setValue(item, schemaManager.marshall(defaultValue, schemaManager.schemaDatatypes.stringQName));
return item;
}
// If null or undefined, and nillable, we set xsi:nil="true"
// and return the element
if (nillable && value === null && isRequired == true)
{
item = createElement(name);
setValue(item, null);
return item;
}
return null;
}
/**
* @private
*/
public function getAttribute(parent:*, name:*):*
{
return getValue(parent, name);
}
/**
* @private
*/
public function hasAttribute(parent:*, name:*):Boolean
{
return (getAttribute(parent, name) !== undefined);
}
/**
* @private
*/
public function setAttribute(parent:XML, name:*, value:*):void
{
if (value != null)
parent.@[name] = value.toString();
}
/**
* @private
*/
public function getProperties(value:*):Array
{
var classInfo:Object = ObjectUtil.getClassInfo(value as Object, null, {includeReadOnly:false});
return classInfo.properties;
}
/**
* Returns a single XML node with the given name
*
* @private
*/
public function createElement(name:*):XML
{
var element:XML;
var elementName:QName;
if (name is QName)
elementName = name as QName;
else
elementName = new QName("", name.toString());
element = <{elementName.localName} />;
if (elementName.uri != null && elementName.uri.length > 0)
{
var prefix:String = schemaManager.getOrCreatePrefix(elementName.uri);
var ns:Namespace = new Namespace(prefix, elementName.uri);
element.setNamespace(ns);
}
return element;
}
/**
* @private
*/
public function getSimpleValue(parent:*, name:*):*
{
var simpleValue:* = getValue(parent, name);
// Support legacy _value property for simpleContent
if (simpleValue === undefined)
simpleValue = getValue(parent, "_value");
return simpleValue;
}
/**
* Determines whether a value should be representable as a single, simple
* value, otherwise the object is regarded as "complex" and contains
* child values referenced by index or name.
*
* @private
*/
public function isSimpleValue(value:*):Boolean
{
if (value is String || value is Number || value is Boolean
|| value is Date || value is int || value is uint
|| value is ByteArray)
{
return true;
}
return false;
}
/**
* @private
*/
public function getValue(parent:*, name:*):*
{
var value:*;
if (parent is XML || parent is XMLList)
{
var node:XMLList = parent[name];
if (node.length() > 0)
value = node;
}
else if (TypeIterator.isIterable(parent))
{
// We may have an associative Array
if (parent.hasOwnProperty(name) && parent[name] !== undefined)
{
value = resolveNamedProperty(parent, name);
}
else
{
// Otherwise, we just return the value as this may be for an
// ArrayOfSomeType that needs special casing to map directly
// to an Array without a wrapper type
value = parent;
}
}
else if (!isSimpleValue(parent))
{
// We only support the public namespace for now
if (name is QName)
name = QName(name).localName;
value = resolveNamedProperty(parent, name);
}
else
{
// FIXME: Shouldn't this be an error condition?
value = parent;
}
return value;
}
/**
* @private
*/
public function hasValue(parent:*, name:*):Boolean
{
return (getValue(parent, name) !== undefined);
}
/**
* @private
*/
public function containsNodeByName(list:XMLList, name:QName, strict:Boolean=false):Boolean
{
var currentURI:String = schemaManager.currentSchema.targetNamespace.uri;
for each (var node:XML in list)
{
if (strict || (name.uri != "" && name.uri != null))
{
// If we need strict comparisons, or if name is qualified, and
// not in the default namespace, we match the full QName. However,
// elements already contained in the encoded XMLList could be
// unqualified, so to match them against a qualified name, we use
// the target namespace of the current schema used in encoding,
// which will be the namespace the unqualified elements assume.
if (node.name().uri == "" && currentURI == name.uri)
{
//compare by localName
if (node.name().localName == name.localName)
return true;
}
else if (node.name() == name)
{
return true;
}
}
else
{
// If we only have a localName, and don't need strict comparisons,
// look for any node with that localName, regardless of namespace.
if (node.name().localName == name.localName)
return true;
}
}
return false;
}
/**
* Looks up value by name on a complex parent object, considering that the
* name might have to be prepended with an underscore.
* @private
*/
public function resolveNamedProperty(parent:*, name:*):*
{
var value:*;
var fallbackName:String = null;
if (!isSimpleValue(parent))
{
try
{
value = parent[name];
// If a value by this name is not defined on a dynamic object,
// try looking up with an underscore.
if (value === undefined)
fallbackName = "_" + name.toString();
}
catch (e:Error)
{
// If a property with that name doesn't exist on a non-dynamic
// object, an error will be thrown. We should still try the
// fallback name.
fallbackName = "_" + name.toString();
}
if (fallbackName != null && parent.hasOwnProperty(fallbackName))
value = parent[fallbackName];
}
return value;
}
/**
* Assigns value to an XML node.
*
* @param parent The node to assign to. Must be either XML or XMLList.
* If XMLList, it must contain at least one XML element. The value is
* assigned on the last element in the list. If XML, the value is assigned
* directly on parent.
* @param value The value to assign on the parent. If null, the xsi:nil
* attribute is set on the parent. If XML or XMLList, the value is appended
* as child node(s) on the parent. Otherwise the string representation of the
* value is appended as a text node. A value that is explicitly undefined is
* skipped.
*
* @private
*/
public function setValue(parent:*, value:*):void
{
if (value !== undefined)
{
var currentChild:XML;
if (parent is XML)
currentChild = parent as XML;
else if (parent is XMLList && parent.length() > 0)
currentChild = parent[parent.length()-1];
if (currentChild != null)
{
if (value === null)
{
// set xsi:nil attribute if value is specifically null.
currentChild.@[schemaManager.schemaConstants.nilQName] = "true";
currentChild.addNamespace(constants.xsiNamespace);
}
else if (value is XML || value is XMLList)
{
currentChild.appendChild(value);
}
else if (value !== undefined)
{
// Everything else is treated as simple content, except
// for undefined, which is skipped.
currentChild.appendChild(xmlSpecialCharsFilter(Object(value)));
}
}
}
}
/**
* Appends a value (or list of values) directly as
* members of the parent XMLList. Effectively merges
* two XMLLists.
*
* @private
*/
public function appendValue(parent:XMLList, value:*):void
{
parent[parent.length()] = value;
}
/**
* Checks to see whether a value defines a custom XSI type to be used
* during encoding, otherwise the default type is returned.
*/
protected function getXSIType(value:*):QName
{
var xsiType:QName;
// Allow IXMLSchemaInstance or ObjectProxy to override XSI type
// information, if provided...
if (value != null)
{
if (value is ObjectProxy && value.object_proxy::type != null)
{
xsiType = value.object_proxy::type;
}
else if (value is IXMLSchemaInstance && IXMLSchemaInstance(value).xsiType != null)
{
xsiType = IXMLSchemaInstance(value).xsiType;
}
}
return xsiType;
}
/**
* Record custom XSI type information for this XML node by adding an
* xsi:type attribute with the value set to the qualified type name.
*/
protected function setXSIType(parent:XML, type:QName):void
{
var namespaceURI:String = type.uri;
var prefix:String = schemaManager.getOrCreatePrefix(namespaceURI);
var prefixNamespace:Namespace = new Namespace(prefix, namespaceURI);
parent.addNamespace(prefixNamespace);
parent.@[constants.getXSIToken(constants.typeAttrQName)] = prefix + ":" + type.localName;
}
/**
* @private
*/
protected function deriveXSIType(parent:XML, type:QName, value:*):void
{
}
/**
* @private
* Default implementation of xmlSpecialCharsFilter. Escapes "&" and "<".
*/
private function escapeXML(value:Object):String
{
var str:String = value.toString();
str = str.replace(/&/g, "&").replace(/