blob: bb5b63e702490f1706bb6300906628b74baf3623 [file] [log] [blame]
/*
*
* 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.royale.compiler.internal.tree.mxml;
import org.apache.royale.abc.ABCConstants;
import org.apache.royale.abc.instructionlist.InstructionList;
import org.apache.royale.abc.semantics.Name;
import org.apache.royale.abc.semantics.Namespace;
import org.apache.royale.abc.semantics.Nsset;
import org.apache.royale.compiler.common.PrefixMap;
import org.apache.royale.compiler.internal.as.codegen.InstructionListNode;
import org.apache.royale.compiler.mxml.IMXMLTagAttributeData;
import org.apache.royale.compiler.mxml.IMXMLTagData;
import org.apache.royale.compiler.mxml.IMXMLTextData;
import org.apache.royale.compiler.mxml.IMXMLTextData.TextType;
import org.apache.royale.compiler.mxml.IMXMLUnitData;
import org.apache.royale.compiler.tree.mxml.IMXMLBindingNode;
import org.apache.royale.compiler.tree.mxml.IMXMLSingleDataBindingNode;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Stack;
/**
* Helper class for MXMLXMLNode and MXMLXMLListNode to process their subtrees.
* Will turn the children sub-tree into a String, and a list of DataBinding
* expressions.
*/
class XMLBuilder
{
public XMLBuilder(MXMLInstanceNode parent, IMXMLTagData rootTag, PrefixMap externalPrefixes, MXMLTreeBuilder builder)
{
this.parent = parent;
this.rootTag = rootTag;
this.externalPrefixes = externalPrefixes;
this.builder = builder;
}
private MXMLInstanceNode parent;
private MXMLTreeBuilder builder;
private IMXMLTagData rootTag;
/**
* PrefixMap of the prefix'es defined outside of the contents of the XML
* tag.
*/
PrefixMap externalPrefixes;
/**
* External prefixes which are referenced from inside the XML tag, each
* these must be added to the root tag as xmlns.
*/
Set<String> referencedPrefixes = new HashSet<String>();
private List<IMXMLBindingNode> databindings = new ArrayList<IMXMLBindingNode>();
/**
* Process an MXMLTagData - this will write a String representation of the
* tag into the StringWriter passed in. This will strip out any databinding
* expressions from the String, TODO: databinding - add the databinding
* expressions as children of the MXMLXMLNode, and also record what the
* TODO: target expressions for those are (these are the expressions to set
* the value in the XML object when the TODO: PropertyChange event fires).
*/
void processNode(IMXMLTagData tag,
StringWriter sw)
{
sw.write('<');
if (tag.isCloseTag())
sw.write('/');
sw.write(tag.getName());
String tagPrefix = tag.getPrefix();
// lookup the prefix in case it's defined elsewhere in the document outside the XML tag
if (tagPrefix != null)
lookupPrefix(tagPrefix, tag);
List<IMXMLTagAttributeData> attrs = getAttributes(tag);
for (IMXMLTagAttributeData attr : attrs)
{
sw.write(' ');
sw.write(attr.getName());
sw.write('=');
sw.write('"');
sw.write(attr.getRawValue());
sw.write('"');
// lookup the prefix in case it's defined outside the XML tag
String prefix = attr.getPrefix();
if (prefix != null)
lookupPrefix(prefix, tag);
}
StringWriter childrenSW = new StringWriter();
for (IMXMLUnitData unit = tag.getFirstChildUnit(); unit != null; unit = unit.getNextSiblingUnit())
{
processNode(unit, childrenSW);
}
if (tag == rootTag)
{
// If we're the root tag, then add an xmlns for each prefix that was referenced by one of our
// children, but was defined elsewhere in the document (like in the Application tag).
for (String prefix : referencedPrefixes)
{
String uri = externalPrefixes.getNamespaceForPrefix(prefix);
if (uri != null)
{
sw.write(" xmlns");
if (!prefix.isEmpty())
sw.write(":");
sw.write(prefix);
sw.write("=\"");
sw.write(uri);
sw.write('\"');
}
}
}
if (tag.isEmptyTag())
{
sw.write("/>");
}
else
{
sw.write('>');
}
sw.write(childrenSW.toString());
IMXMLTagData endTag = tag.findMatchingEndTag();
if (endTag != null)
{
processNode(endTag, sw);
}
}
/**
* Do a lookup of a prefix, starting with the specified tag. This method
* will record if the prefix is defined outside of the <fx:XML> tag. If it
* is defined outside of the tag, then the prefix will need to be added to
* the root XML tag as an 'xmlns' so that it will still be accessible.
*
* @param prefix the prefix to look for
* @param tag the tag to start looking in
*/
void lookupPrefix(String prefix, IMXMLTagData tag)
{
while (tag != null)
{
PrefixMap pm = tag.getPrefixMap();
if (pm != null && pm.containsPrefix(prefix))
// we found the prefix, and haven't gone past the root tag, so don't need to do anything special
return;
else if (tag == rootTag)
// don't look past the root tag
tag = null;
else
tag = tag.getParentTag();
}
// We will only get here if we have traversed past the root XML tag, and haven't found the prefix yet
// in that case, record the prefix if its defined somewhere else in the MXML document, so we can
// make sure it ends up in the resulting XML literal.
if (externalPrefixes.containsPrefix(prefix))
referencedPrefixes.add(prefix);
}
/**
* Process an IMXMLTextData - this will write a String representation of the
* tag into the StringWriter passed in. This will strip out any databinding
* expressions from the String. This will write out only CDATA and TEXT
* TextDatas Other kinds of text data are not output into the resulting XML
* object. TODO: databinding - add the databinding expressions as children
* of the MXMLXMLNode, and also record what the TODO: target expressions for
* those are (these are the expressions to set the value in the XML object
* when the TODO: PropertyChange event fires).
*/
void processNode(IMXMLTextData tag,
StringWriter sw)
{
switch (tag.getTextType())
{
case CDATA:
// For CDATA, just write out the text
sw.write(tag.getContent());
break;
case TEXT:
{
IMXMLSingleDataBindingNode db = null;
if ((db = parseBindingExpression(tag)) != null)
{
// do databinding stuff:
// 1. Walk up parent chain to compute target expression
// 2. Parse databinding expression
// 3. Save off both those pieces of data for use during codegen
databindings.add(generateBindingNode(tag, db));
if (!isOnlyTextChild(tag))
{
// Write out an empty CDATA section, so the tag will have the right
// number of text children for the binding to target.
sw.write("<![CDATA[]]>");
}
}
else
{
sw.write(replaceBindingEscapes(tag.getContent()));
}
}
break;
// Everything else gets stripped out
}
}
/**
* Generate an MXMLBindingNode to represent the binding expression in the
* IMXMLTextData passed in.
*
* @param tag the TextData that is the destination of the binding expression
* @param dbnode the DataBinding Node that contains the source expression
* @return An MXMLBindingNode with expressions for the source and
* destination
*/
private MXMLBindingNode generateBindingNode(IMXMLTextData tag, IMXMLSingleDataBindingNode dbnode)
{
return generateBindingNode(tag, null, dbnode);
}
/**
* Generate an MXMLBindingNode to represent the binding expression in the
* MXMLTagAttributeData passed in.
*
* @param attr the Attribute that is the destination of the binding
* expression
* @param dbnode the DataBinding Node that contains the source expression
* @return An MXMLBindingNode with expressions for the source and
* destination
*/
private MXMLBindingNode generateBindingNode(IMXMLTagAttributeData attr, IMXMLSingleDataBindingNode dbnode)
{
return generateBindingNode(attr.getParent(), attr, dbnode);
}
/**
* Generate the destination expression, and build the MXMLBindingNode
*
* @param tag The unit data that is the target of the databinding
* expression, if an attribute is passed in this should be the TagData
* containing the attribute
* @param attr The attribute that is the target of the databinding
* expression, or null if we are not targeting an attribute
* @param dbnode The DataBindingNode that contains the source expression
* @return An MXMLBindingNode with the source and destination expressions
*/
private MXMLBindingNode generateBindingNode(IMXMLUnitData tag, IMXMLTagAttributeData attr, IMXMLSingleDataBindingNode dbnode)
{
// Build the destination expression
InstructionListNode destExpr = getTargetExprNode(tag, attr);
MXMLBindingNode bindingNode = new MXMLBindingNode(parent);
MXMLBindingAttributeNode target = new MXMLBindingAttributeNode(bindingNode, destExpr);
destExpr.setParent(target);
MXMLBindingAttributeNode source = new MXMLBindingAttributeNode(bindingNode, dbnode.getExpressionNode());
bindingNode.setDestinationAttributeNode(target);
bindingNode.setSourceAttributeNode(source);
return bindingNode;
}
/**
* Attempt to parse a binding expression from the passed in text data
*
* @param text The Text Data to parse
* @return An IMXMLDataBindingNode that was parsed from text, or null if no
* databinding expression was found
*/
private IMXMLSingleDataBindingNode parseBindingExpression(IMXMLTextData text)
{
Object o = MXMLDataBindingParser.parse(parent, text, text.getFragments(builder.getProblems()), builder.getProblems(),
builder.getWorkspace(), builder.getMXMLDialect(),
builder.getProject());
if (o instanceof IMXMLSingleDataBindingNode)
{
return ((IMXMLSingleDataBindingNode)o);
}
return null;
}
/**
* Attempt to parse a binding expression from the passed in attribute
*
* @param attr The Tag Attribute Data to parse
* @return An IMXMLDataBindingNode that was parsed from attr, or null if no
* databinding expression was found
*/
private IMXMLSingleDataBindingNode parseBindingExpression(IMXMLTagAttributeData attr)
{
Object o = MXMLDataBindingParser.parse(parent, attr, attr.getValueFragments(builder.getProblems()), builder.getProblems(),
builder.getWorkspace(), builder.getMXMLDialect(),
builder.getProject());
if (o instanceof IMXMLSingleDataBindingNode)
{
return ((IMXMLSingleDataBindingNode)o);
}
return null;
}
/**
* Helper to build a InstructionListNode
*
* @param data The unit data to target
* @param attr The attr to target, or null if we aren't targeting an attr
* @return An InstructionListNode that can be used as the destination
* expression for an MXMLBindingNode
*/
private InstructionListNode getTargetExprNode(IMXMLUnitData data, IMXMLTagAttributeData attr)
{
InstructionListNode expr = null;
InstructionList il = getTargetInstructions(data, attr);
if (il != null)
{
expr = new InstructionListNode(il);
}
return expr;
}
/**
* Generate the instructions to set the target expression based on the Data
* and Attribute passed in. This will walk up the parent chain to the root
* tag, and then proceed to emit get instructions for all parents from the
* root tag down. For the actual target, instructions to set the value will
* be generated. If an attribute is passed in then the set instructions will
* target the attribute instead
*
* @param data The UnitData to target
* @param attr The attribute to target, or null if there is no attribute
* @return An InstructionList that contains the instructions to set the
* target expression
*/
private InstructionList getTargetInstructions(IMXMLUnitData data, IMXMLTagAttributeData attr)
{
IMXMLUnitData d = data;
Stack<IMXMLUnitData> parentStack = new Stack<IMXMLUnitData>();
if (isOnlyTextChild(d))
{
// If we are the only text child of a TagData, then start with the TagData,
// as that is what we'll be setting
d = d.getParentUnitData();
}
IMXMLUnitData target = d;
// push parents onto a stack, so we can walk down from the parent later
while (d != null)
{
parentStack.add(d);
d = d == rootTag ? null : d.getParentUnitData();
}
InstructionList il = new InstructionList();
// we're always going to start with "this"
il.addInstruction(ABCConstants.OP_getlocal0);
// Walk down the parent stack, and emit get instructions for each tag
// except for the last one, which is the one we're targeting
while (parentStack.size() > 1)
{
IMXMLUnitData unitData = parentStack.pop();
if (unitData instanceof IMXMLTagData)
{
generateGetInstructions(il, (IMXMLTagData)unitData);
}
}
if (target instanceof IMXMLTagData)
{
// Targeting a Tag
if (attr == null)
{
// Just setting the tag value
generateSetInstructions(il, (IMXMLTagData)target);
}
else
{
// We have an attr, do a get for the tag, and a set
// for the attr
generateGetInstructions(il, (IMXMLTagData)target);
generateSetInstructions(il, attr);
}
}
else if (target instanceof IMXMLTextData)
{
// We're targeting a TextData
generateSetInstructions(il, (IMXMLTextData)target);
}
return il;
}
/**
* Generate the instructions to place the tag on the stack. This assumes
* that the base expr is already in the instruction list
* <a><b><c></c></b></a> so if called on the C node above, it would assume
* the instructions to place b on the stack were already generated - this
* method will only compute the instructions to get c from b
*/
private void generateGetInstructions(InstructionList il, IMXMLTagData tag)
{
if (tag == rootTag)
{
il.addInstruction(ABCConstants.OP_getproperty, getNameForTag(tag));
}
else
{
il.addInstruction(ABCConstants.OP_getproperty, getNameForTag(tag));
int index = getIndexOfTag(tag);
il.addInstruction(ABCConstants.OP_getproperty, new Name(String.valueOf(index)));
}
}
/**
* Generate the instructions to set the value of a tag. This assumes that
* the base expr is already in the instruction list <a><b><c></c></b></a> so
* if called on the C node above, it would assume the instructions to place
* b on the stack were already generated - this method will only compute the
* instructions to set c in b Assumes that the new value is stored in local
* 1 (will use OP_getlocal1 to get it). This code will be in a function that
* has 1 argument, which is the new value, so we know it's passed in as the
* first local.
*/
private void generateSetInstructions(InstructionList il, IMXMLTagData tag)
{
if (tag == rootTag)
{
il.addInstruction(ABCConstants.OP_getlocal1);
il.addInstruction(ABCConstants.OP_setproperty, getNameForTag(tag));
}
else
{
il.addInstruction(ABCConstants.OP_getproperty, getNameForTag(tag));
il.addInstruction(ABCConstants.OP_getlocal1);
int index = getIndexOfTag(tag);
il.addInstruction(ABCConstants.OP_setproperty, new Name(String.valueOf(index)));
}
}
/**
* Generate the instructions to set the value of a tag. This assumes that
* the base expr is already in the instruction list <a><b><c
* f="{blah}"></c></b></a> so if called on the f node above, it would assume
* the instructions to place c on the stack were already generated - this
* method will only compute the instructions to set f in c Assumes that the
* new value is stored in local 1 (will use OP_getlocal1 to get it). This
* code will be in a function that has 1 argument, which is the new value,
* so we know it's passed in as the first local.
*/
private void generateSetInstructions(InstructionList il, IMXMLTagAttributeData attr)
{
il.addInstruction(ABCConstants.OP_getlocal1);
il.addInstruction(ABCConstants.OP_setproperty, getNameForAttr(attr));
}
/**
* Generate the instructions to set the value of a text data. This assumes
* that the base expr is already in the instruction list
* <a><b><c>...<![CDATA[]]></![CDATA[]]></c></b></a> so if called on the
* CDATA node above, it would assume the instructions to place c on the
* stack were already generated - this method will only compute the
* instructions to set the CDATA in c This will compute the index of the
* cdata, and then emit code like: .text()[index] = value Assumes that the
* new value is stored in local 1 (will use OP_getlocal1 to get it). This
* code will be in a function that has 1 argument, which is the new value,
* so we know it's passed in as the first local.
*/
private void generateSetInstructions(InstructionList il, IMXMLTextData text)
{
il.addInstruction(ABCConstants.OP_callproperty, new Object[] {new Name("text"), 0});
il.addInstruction(ABCConstants.OP_getlocal1);
int index = getIndexOfText(text);
il.addInstruction(ABCConstants.OP_setproperty, new Name(String.valueOf(index)));
}
/**
* Get the index of a tag. Grabs the parent tag, and iterates it's children
* to find out what the index of the tag passed in should be
*/
private int getIndexOfTag(IMXMLTagData tag)
{
IMXMLTagData parent = tag.getParentTag();
int index = 0;
for (IMXMLTagData d = parent.getFirstChild(true); d != null; d = d.getNextSibling(true))
{
if (d == tag)
break;
else if (d.getName().equals(tag.getName()))
++index;
}
return index;
}
/**
* Get the index of a text data. Grabs the parent, and iterates it's
* children to find out what the index of the text data passed in should be
*/
private int getIndexOfText(IMXMLTextData text)
{
IMXMLUnitData parent = text.getParentUnitData();
IMXMLTagData parentTag = parent instanceof IMXMLTagData ? (IMXMLTagData)parent : null;
int index = 0;
if (parentTag != null)
{
for (IMXMLUnitData d = parentTag.getFirstChildUnit(); d != null; d = d.getNextSiblingUnit())
{
if (d == text)
break;
else if (d instanceof IMXMLTextData && ((IMXMLTextData)d).getTextType() == TextType.CDATA)
++index;
}
}
return index;
}
/**
* Helper to determine if a give MXMLUnitData is the only Text child of an
* MXMLTagData This implies special, different processing from normal Text
* Datas.
*/
private boolean isOnlyTextChild(IMXMLUnitData child)
{
if (child instanceof IMXMLTextData && ((IMXMLTextData)child).getTextType() == TextType.TEXT)
{
IMXMLUnitData p = child.getParentUnitData();
IMXMLTagData parent = p instanceof IMXMLTagData ? (IMXMLTagData)p : null;
if (parent != null)
{
return parent.getFirstChildUnit() == child && child.getNextSiblingUnit() == null;
}
}
return false;
}
/**
* Generate an AET Name that corresponds to the tag passed in
*/
private Name getNameForTag(IMXMLTagData tag)
{
if (tag == rootTag)
{
return new Name(parent.getEffectiveID());
}
else
{
String uri = tag.getURI();
if (uri != null)
{
return new Name(new Namespace(ABCConstants.CONSTANT_Namespace, uri), tag.getShortName());
}
else
{
return new Name(tag.getShortName());
}
}
}
/**
* Generate an AET Name for the attr passed in
*/
private Name getNameForAttr(IMXMLTagAttributeData attr)
{
String uri = attr.getURI();
if (uri != null)
{
return new Name(ABCConstants.CONSTANT_QnameA, new Nsset(new Namespace(ABCConstants.CONSTANT_Namespace, uri)), attr.getShortName());
}
else
{
return new Name(ABCConstants.CONSTANT_QnameA, new Nsset(new Namespace(ABCConstants.CONSTANT_Namespace, "")), attr.getShortName());
}
}
/**
* replace backslashes for curly braces and @ with &#7d; &#7b;
*
* @param toClean the string to clean
* @return the cleaned string
*/
public static String replaceBindingEscapes(String toClean)
{
toClean = cleanupEscapedCharForXML('{', toClean);
toClean = cleanupEscapedCharForXML('}', toClean);
toClean = cleanupEscapedCharForXML('@', toClean);
return toClean;
}
/**
* Get rid of backslashes that were escaping the specified character
* @param toClean
* @return the cleaned string
*/
private static String cleanupEscapedCharForXML(char escapedChar, String toClean)
{
//if there's no char to begin with or no escape character we can just return the orig string
if (toClean == null || toClean.indexOf(escapedChar) == -1 || toClean.indexOf('\\') == -1)
{
return toClean;
}
StringBuilder buf = new StringBuilder(toClean.length());
char[] chars = toClean.toCharArray();
int i;
for (i = 0; i < chars.length - 1; ++i)
{
if (chars[i] != '\\' || chars[i+1] != escapedChar)
{
buf.append(chars[i]);
} else {
buf.append("&#x" + Integer.toString((chars[i+1]), 16) + ";");
i++;
}
}
if (i == chars.length - 1) {
buf.append(chars[chars.length - 1]);
}
return buf.toString();
}
/**
* Get a list of attributes, filtering out the attributes that have data
* binding values.
*
* @param tag The
* @return
*/
List<IMXMLTagAttributeData> getAttributes(IMXMLTagData tag)
{
IMXMLTagAttributeData[] rawAttrs = tag.getAttributeDatas();
if (rawAttrs != null)
{
ArrayList<IMXMLTagAttributeData> attrs = new ArrayList<IMXMLTagAttributeData>(rawAttrs.length);
for (IMXMLTagAttributeData attr : rawAttrs)
{
IMXMLSingleDataBindingNode db = null;
if ((db = parseBindingExpression(attr)) != null)
{
// do databinding stuff:
// 1. Walk up parent chain to compute target expression
// 2. Parse databinding expression
// 3. Save off both those pieces of data for use during codegen
databindings.add(generateBindingNode(attr, db));
}
else
{
attrs.add(attr);
}
}
return attrs;
}
return Collections.emptyList();
}
void processNode(IMXMLUnitData node, StringWriter sw)
{
if (node instanceof IMXMLTagData)
processNode((IMXMLTagData)node, sw);
else if (node instanceof IMXMLTextData)
processNode((IMXMLTextData)node, sw);
}
public List<IMXMLBindingNode> getDatabindings()
{
return databindings;
}
}