blob: 14885d6af3c4af78a2034d1012d00b079bde3e9a [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.mxml;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import org.apache.commons.io.IOUtils;
import org.apache.royale.compiler.common.MutablePrefixMap;
import org.apache.royale.compiler.common.PrefixMap;
import org.apache.royale.compiler.filespecs.FileSpecification;
import org.apache.royale.compiler.filespecs.IFileSpecification;
import org.apache.royale.compiler.internal.parsing.mxml.BalancingMXMLProcessor;
import org.apache.royale.compiler.internal.parsing.mxml.MXMLToken;
import org.apache.royale.compiler.internal.parsing.mxml.MXMLTokenizer;
import org.apache.royale.compiler.internal.parsing.mxml.MXMLUnitDataIterator;
import org.apache.royale.compiler.mxml.IMXMLData;
import org.apache.royale.compiler.mxml.IMXMLTagAttributeData;
import org.apache.royale.compiler.mxml.IMXMLTagData;
import org.apache.royale.compiler.mxml.IMXMLUnitData;
import org.apache.royale.compiler.parsing.MXMLTokenTypes;
import org.apache.royale.compiler.problems.ICompilerProblem;
import org.apache.royale.compiler.problems.SyntaxProblem;
import org.apache.royale.utils.FastStack;
import org.apache.royale.utils.FastStack.IFastStackDecorator;
/**
* Encapsulation of an MXML file, with individual units for each open tag, close
* tag, and block of text.
*/
public class MXMLData implements IMXMLData
{
private final class TokenizerPayload
{
private List<MXMLToken> tokens;
private PrefixMap prefixMap;
public TokenizerPayload(List<MXMLToken> tokens, PrefixMap prefixMap)
{
this.tokens = tokens;
this.prefixMap = prefixMap;
}
public List<MXMLToken> getMXMLTokens()
{
return tokens;
}
public PrefixMap getPrefixMap()
{
return prefixMap;
}
}
/**
* A cursor represents the offset into the document and the index of the
* MXML unit that contains that offset
*/
public class Cursor
{
/**
* Offset into the document
*/
private int offset;
/**
* Index of the unit that contains that offset
*/
private int unitIndex;
/**
* Constructor (points to beginning of the MXMLData)
*/
public Cursor()
{
reset();
}
/**
* Reset cursor to the beginning of the MXMLData
*/
public void reset()
{
offset = 0;
unitIndex = 0;
}
/**
* Set the cursor to a particular offset/unit
*
* @param offset offset into the document
* @param unitIndex index of the unit containing that offset
*/
public void setCursor(int offset, int unitIndex)
{
this.offset = offset;
this.unitIndex = unitIndex;
}
/**
* Get the offset the cursor is pointing to
*
* @return current document offset
*/
public int getOffset()
{
return offset;
}
/**
* Get the index of the unit the cursor is pointing to
*
* @return current unit index
*/
public int getUnitIndex()
{
return unitIndex;
}
}
private String path = null;
/**
* Individual units for each open tag, close tag, and block of text
*/
private IMXMLUnitData[] units;
/**
* Flag indicating that the tokens underlying this structure were fixed
*/
private boolean wasRepaired;
private boolean shouldRepair = true;
/**
* This maps {@link IMXMLTagData} objects to their explicit
* {@link PrefixMap} if it exist
*/
private HashMap<IMXMLTagData, PrefixMap> nsMap;
/**
* The cursor holds the result of last offset lookup into the MXMLData (see
* findNearestUnit, below). When an edit happens that causes the code
* coloring engine to recompute everything from the edit position to the end
* of the document, it will request MXMLOffsetInformations in order. Having
* the cursor speeds this up tremendously.
*/
private Cursor cursor;
/**
* The dialect of MXML being used.
*/
private MXMLDialect mxmlDialect;
private Collection<ICompilerProblem> problems = new ArrayList<ICompilerProblem>(2);
/**
* Tells us if we are currently processing full MXML, or a fragment
*/
private boolean fullContent = true;
/**
* Constructor
*
* @param tokens MXML tokens to build the MXMLData object from
* @param map the {@link PrefixMap} for the document, containing the
* namespace/prefix mappings
*/
public MXMLData(List<MXMLToken> tokens, PrefixMap map, IFileSpecification fileSpec)
{
path = fileSpec.getPath();
init(tokens, map);
}
public MXMLData(List<MXMLToken> tokens, PrefixMap map, IFileSpecification fileSpec, boolean shouldRepair)
{
path = fileSpec.getPath();
this.shouldRepair = false;
init(tokens, map);
}
/**
* Constructor
*
* @param mxmlText input build our {@link MXMLData} from
*/
public MXMLData(int startOffset, Reader mxmlText, IFileSpecification fileSpec)
{
TokenizerPayload payload = getTokens(startOffset, mxmlText);
path = fileSpec.getPath();
init(payload.getMXMLTokens(), payload.getPrefixMap());
}
/**
* Constructor
*
* @param mxmlText input build our {@link MXMLData} from
*/
public MXMLData(int startOffset, Reader mxmlText, String path, boolean fullContent)
{
this.fullContent = fullContent;
TokenizerPayload payload = getTokens(startOffset, mxmlText);
this.path = path;
init(payload.getMXMLTokens(), payload.getPrefixMap());
}
public MXMLData(Reader mxmlText, IFileSpecification fileSpec)
{
this(0, mxmlText, fileSpec);
}
public MXMLData(Reader mxmlText, String path)
{
// Some clients of MXML data pass in a null IPath for the source path.
// Specifically this seems to happen in the code that builds a new
// mxml document from a template.
this(0, mxmlText, path != null ? path : null, true);
}
/**
* Constructor
*
* @param specification input build our {@link MXMLData} from
*/
public MXMLData(IFileSpecification specification)
{
TokenizerPayload payload;
try
{
path = specification.getPath();
payload = getTokens(specification.createReader());
init(payload.getMXMLTokens(), payload.getPrefixMap());
}
catch (FileNotFoundException e)
{
//do nothing
}
}
private void init(List<MXMLToken> tokens, PrefixMap map)
{
mxmlDialect = MXMLDialect.getMXMLDialect(map);
initializeFromTokens(tokens);
}
/**
* Update the version of the mxml data
*
* @param map the prefix map
*/
public void updateMXMLVersion(PrefixMap map)
{
mxmlDialect = MXMLDialect.getMXMLDialect(map);
}
/**
* Get the namespaces defined on the root tag
*
* @param reader The reader which would provide the root tag information
* @return {@link PrefixMap} for the root tag
* @throws IOException error
*/
public static PrefixMap getRootNamespaces(Reader reader) throws IOException
{
MXMLTokenizer tokenizer = new MXMLTokenizer();
try
{
tokenizer.setReader(reader);
return tokenizer.getRootTagPrefixMap().clone();
}
finally
{
tokenizer.close();
}
}
protected TokenizerPayload getTokens(Reader reader)
{
return getTokens(0, reader);
}
protected TokenizerPayload getTokens(int startOffset, Reader reader)
{
List<MXMLToken> tokens = null;
MXMLTokenizer tokenizer = new MXMLTokenizer(startOffset);
try
{
tokenizer.setIsRepairing(true);
tokens = tokenizer.parseTokens(reader);
wasRepaired = tokenizer.tokensWereRepaired();
return new TokenizerPayload(tokens, tokenizer.getPrefixMap());
}
finally
{
IOUtils.closeQuietly(tokenizer);
}
}
@Override
public MXMLDialect getMXMLDialect()
{
return mxmlDialect;
}
@Override
public PrefixMap getPrefixMapForData(IMXMLTagData data)
{
PrefixMap result = nsMap.get(data);
if (result != null)
return result;
if (data.isCloseTag())
{
IMXMLTagData openTagData = data.getParent().findTagOrSurroundingTagContainingOffset(data.getAbsoluteStart());
if (openTagData != null)
return nsMap.get(openTagData);
}
return null;
}
void removePrefixMappingForTag(IMXMLTagData data)
{
nsMap.remove(data);
}
void clearPrefixMappings()
{
nsMap.clear();
}
public void setPrefixMappings(HashMap<IMXMLTagData, PrefixMap> map)
{
if (nsMap != null)
nsMap.clear();
nsMap = map;
}
public Map<IMXMLTagData, PrefixMap> getTagToPrefixMap()
{
return nsMap;
}
/**
* Returns a collection of prefix->namespaces mappings found within this
* document. This map DOES NOT maintain order, and for more fine-grained
* information, the getPrefixMap call on individual {@link IMXMLTagData} and
* {@link IMXMLTagAttributeData} objects should be called
*
* @return a prefix map
*/
public PrefixMap getDocumentPrefixMap()
{
MutablePrefixMap map = new MutablePrefixMap();
for (PrefixMap tagMap : nsMap.values())
{
assert tagMap != null;
map.addAll(tagMap);
}
return map.toImmutable();
}
/**
* Returns the PrefixMap for the root tag of this {@link MXMLData} object
*
* @return a {@link PrefixMap} or null
*/
public PrefixMap getRootTagPrefixMap()
{
IMXMLTagData rootTag = getRootTag();
if (rootTag != null)
{
return nsMap.get(rootTag);
}
return null;
}
@Override
public Collection<ICompilerProblem> getProblems()
{
return problems;
}
public boolean hasProblems()
{
return problems.size() > 0;
}
/**
* Determines if this data was built from a source that was repaired
*
* @return true if the underlying source was repaired
*/
public boolean isDataRepaired()
{
return wasRepaired;
}
public void setWasRepaired(boolean wasRepaired)
{
this.wasRepaired = wasRepaired;
}
@Override
public IFileSpecification getFileSpecification()
{
return new FileSpecification(path);
}
@Override
public String getPath()
{
return path;
}
/**
* API to change the path after MXMLData creation. An MXMLDocument makes an
* MXMLData before the actual location is known.
*
* @param path is the absolute path to the backing source file
*/
public void setPath(String path)
{
this.path = path;
}
/**
* Common code from the two constructors
*
* @param tokens A list of MXML tokens.
*/
protected void initializeFromTokens(List<MXMLToken> tokens)
{
cursor = new Cursor();
parse(this, tokens, mxmlDialect, problems);
cursor.reset();
}
/**
* Use the MXML tokens to build MXMLUnitData.
*
* @param data the {@link MXMLData} object
* @param tokens the list of tokens to build this data from
* @param dialect the {@link MXMLDialect} we are working against
* @param incremental true if this data is being built incrementally. All
* location updates will need to be done outside this element
*/
private void parse(MXMLData data, List<MXMLToken> tokens, MXMLDialect dialect, Collection<ICompilerProblem> problems)
{
ArrayList<MXMLUnitData> units = new ArrayList<MXMLUnitData>(tokens.size() / 6);
nsMap = new HashMap<IMXMLTagData, PrefixMap>();
MXMLUnitData unit = null;
MXMLToken currentComment = null;
FastStack<Integer> depth = new FastStack<Integer>(tokens.size() / 8);
IFileSpecification spec = new FileSpecification(data.getPath() != null ? data.getPath() : "");
depth.setStackDecorator(new IFastStackDecorator<Integer>() {
@Override
public Integer decorate(Integer e)
{
if (e == null)
return -1;
return e;
}
});
int index = -1;
int balancingIndex = 0;
depth.push(index);
ListIterator<MXMLToken> tokenIterator = tokens.listIterator();
BalancingMXMLProcessor processor = new BalancingMXMLProcessor(getFileSpecification(), problems);
while (tokenIterator.hasNext())
{
MXMLToken token = tokenIterator.next();
switch (token.getType())
{
case MXMLTokenTypes.TOKEN_ASDOC_COMMENT:
{
currentComment = token;
//treat this like text.
unit = new MXMLTextData(token);
units.add(unit);
index++;
if (fullContent)
{
unit.setParentUnitDataIndex(depth.peek());
unit.setLocation(data, index);
}
break;
}
case MXMLTokenTypes.TOKEN_COMMENT:
case MXMLTokenTypes.TOKEN_CDATA:
case MXMLTokenTypes.TOKEN_WHITESPACE:
{
//treat this like text.
unit = new MXMLTextData(token);
units.add(unit);
index++;
if (fullContent)
{
unit.setParentUnitDataIndex(depth.peek());
unit.setLocation(data, index);
}
break;
}
case MXMLTokenTypes.TOKEN_OPEN_TAG_START:
{
unit = new MXMLTagData();
MutablePrefixMap map = ((MXMLTagData)unit).init(this, token, tokenIterator, dialect, spec, problems);
((MXMLTagData)unit).setCommentToken(currentComment);
currentComment = null;
units.add(unit);
index++;
if (fullContent)
{
unit.setParentUnitDataIndex(depth.peek());
unit.setLocation(data, index);
if (!((MXMLTagData)unit).isEmptyTag())
processor.addOpenTag((MXMLTagData)unit, balancingIndex);
}
if (map != null)
nsMap.put((MXMLTagData)unit, map.toImmutable());
if (!((MXMLTagData)unit).isEmptyTag())
{
depth.push(index);
balancingIndex++;
}
break;
}
case MXMLTokenTypes.TOKEN_CLOSE_TAG_START:
{
unit = new MXMLTagData();
((MXMLTagData)unit).init(this, token, tokenIterator, dialect, spec, problems);
((MXMLTagData)unit).setCommentToken(currentComment);
if (!((MXMLTagData)unit).isEmptyTag())
{
depth.pop();
balancingIndex--;
}
index++;
if (fullContent)
{
unit.setLocation(data, index);
unit.setParentUnitDataIndex(depth.peek());
processor.addCloseTag((MXMLTagData)unit, balancingIndex);
}
currentComment = null;
units.add(unit);
break;
}
case MXMLTokenTypes.TOKEN_TEXT:
{
unit = new MXMLTextData(token);
units.add(unit);
index++;
if (fullContent)
{
unit.setParentUnitDataIndex(depth.peek());
unit.setLocation(data, index);
}
break;
}
case MXMLTokenTypes.TOKEN_PROCESSING_INSTRUCTION:
{
unit = new MXMLInstructionData(token);
units.add(unit);
index++;
if (fullContent)
{
unit.setParentUnitDataIndex(depth.peek());
unit.setLocation(data, index);
}
break;
}
default:
{
problems.add(new SyntaxProblem(token));
break;
}
}
}
this.units = units.toArray(new IMXMLUnitData[0]);
if (fullContent && shouldRepair)
{
this.units = processor.balance(this.units, this, nsMap);
if (processor.wasRepaired())
{ //repaired, so let's rebuild our prefix maps and tag depths
refreshPositionData();
}
}
}
/**
* Used to rebuild the structures inside of the MXMLData, refreshing prefix
* maps, depth and position data. Should only be used after calls that
* rebuild the internal structure are called
*/
public void refreshPositionData()
{
FastStack<Integer> depth = new FastStack<Integer>(units.length / 2);
depth.setStackDecorator(new IFastStackDecorator<Integer>() {
@Override
public Integer decorate(Integer e)
{
if (e == null)
return -1;
return e;
}
});
depth.clear();
depth.push(-1);
for (int i = 0; i < units.length; i++)
{
if (units[i] instanceof IMXMLTagData)
{
IMXMLTagData currentTag = (IMXMLTagData)units[i];
if (currentTag.isCloseTag())
{
if (!currentTag.isEmptyTag())
depth.pop();
}
((MXMLTagData)currentTag).setParentUnitDataIndex(depth.peek());
((MXMLTagData)currentTag).setLocation(this, i);
if (currentTag.isOpenTag())
{
if (!currentTag.isEmptyTag())
{
depth.push(i);
}
}
}
else
{
((MXMLUnitData)units[i]).setParentUnitDataIndex(depth.peek());
((MXMLUnitData)units[i]).setLocation(this, i);
}
}
}
@Override
public IMXMLUnitData[] getUnits()
{
return units;
}
public Iterator<IMXMLUnitData> getUnitIterator()
{
return new MXMLUnitDataIterator(units);
}
/**
* Replace the list of units in this MXMLData.
*
* @param units units to add
*/
public void setUnits(IMXMLUnitData[] units)
{
this.units = units;
}
@Override
public IMXMLUnitData getUnit(int i)
{
if (i < 0 || i >= units.length)
return null;
return units[i];
}
@Override
public int getNumUnits()
{
return units.length;
}
/**
* If the offset is contained within an MXML unit, get that unit. If it's
* not, then get the first unit that follows the offset.
*
* @param offset test offset
* @return unit that contains (or immediately follows) the offset A few
* subtleties: In Royale we have endeavored to preserve the existing
* definition of "nearest", so that for a given MXML file, royale will find
* the same "nearest" unit.
*/
protected IMXMLUnitData findNearestUnit(int offset)
{
// Use the cursor as a fast search hint. But only if the cursor is at or before the
// are of interest.
// TODO: do we care that this is not thread safe?
int startOffset = 0;
if (cursor.getOffset() <= offset)
startOffset = cursor.getUnitIndex();
// Sanity check
if (startOffset < 0 || startOffset >= units.length)
startOffset = 0;
// Now iterate though the units and find the first one that is acceptable
IMXMLUnitData ret = null;
for (int i = startOffset; (i < units.length) && (ret == null); i++)
{
IMXMLUnitData unit = units[i];
// unit is a match if it "contains" the offset.
// We are using a somewhat bizarre form of "contains" here, in that we are
// using getStart() and getConentEnd(). This asymmetric mismatch is for several reasons:
// * it's the only way to match the existing (non-royale) behavior
// * If our cursor is before the <, we want to match the tag.
// example: |<foo > will find "foo" as the nearest tag.
// So we need to use start here (not content start)
// * If our cursor is between two tags, we want to match the NEXT one, not the previous one
// example: <bar >|<foo> should match foo, not bar
if (MXMLData.contains(unit.getAbsoluteStart(), unit.getContentEnd(), offset))
{
ret = unit;
}
// if we find a unit that starts after the offset, then it must
// be the "first one after", so return it
else if (unit.getAbsoluteStart() >= offset)
{
ret = unit;
}
}
// If we found something, update the cursor for the next search
if (ret != null)
cursor.setCursor(offset, ret.getIndex());
return ret;
}
/**
* Get the unit that should be referenced when looking at what tags contain
* this offset (i.e. tags that are opened and not closed before this offset)
*
* @param offset test offset
* @return reference unit for containment searches
*/
public IMXMLUnitData findContainmentReferenceUnit(int offset)
{
return findNearestUnit(offset);
}
/**
* Get the unit that contains this offset
*
* @param offset test offset
* @return the containing unit (or null if no unit contains this offset)
*/
public IMXMLUnitData findUnitContainingOffset(int offset)
{
IMXMLUnitData unit = findNearestUnit(offset);
if (unit != null && unit.containsOffset(offset))
return unit;
return null;
}
/**
* Get the open, close, or empty tag that contains this offset. Note that if
* offset is inside a text node, this returns null. If you want the
* surrounding tag in that case, use
* findTagOrSurroundingTagContainingOffset.
*
* @param offset test offset
* @return the containing tag (or null, if no tag contains this offset)
*/
public IMXMLTagData findTagContainingOffset(int offset)
{
IMXMLUnitData unit = findUnitContainingOffset(offset);
if (unit != null && unit.isTag())
return (IMXMLTagData)unit;
return null;
}
@Override
public IMXMLTagData findTagOrSurroundingTagContainingOffset(int offset)
{
IMXMLUnitData unit = findUnitContainingOffset(offset);
if (unit != null)
{
if (unit.isTag())
{
return (IMXMLTagData)unit;
}
else if (unit.isText())
{
IMXMLTagData containingTag = unit.getContainingTag(unit.getAbsoluteStart());
return containingTag;
}
}
return null;
}
/**
* Get the open or empty tag whose attribute list contains this offset. A
* tag's attribute list extends from after the tag name + first whitespace
* until before the closing ">" or "/>".
*
* @param offset test offset
* @return tag whose attribute list contains this offset (or null, if the
* offset isn't in any attribute lists
*/
public IMXMLTagData findAttributeListContainingOffset(int offset)
{
IMXMLTagData tag = findTagContainingOffset(offset);
if (tag != null && tag.isOpenTag())
return tag.isOffsetInAttributeList(offset) ? tag : null;
return null;
}
/**
* Test whether the offset is contained within the range from start to end.
* This test excludes the start position and includes the end position,
* which is how you want things to work for code hints.
*
* @param start start of the range (excluded)
* @param end end of the range (included)
* @param offset test offset
* @return true iff the offset is contained in the range
*/
public static boolean contains(int start, int end, int offset)
{
return start < offset && end >= offset;
}
@Override
public IMXMLTagData getRootTag()
{
int n = units.length;
for (int i = 0; i < n; i++)
{
IMXMLUnitData unit = units[i];
if (unit instanceof IMXMLTagData)
return (IMXMLTagData)unit;
}
return null;
}
@Override
public int getEnd()
{
final int n = getNumUnits();
return n > 0 ? getUnit(n - 1).getAbsoluteEnd() : 0;
}
public Cursor getCursor()
{
return cursor;
}
/**
* Verifies that all units (plus all attributes on units that are tags) have
* their source location information set.
* <p>
* This is used only in asserts.
*/
public boolean verify()
{
for (IMXMLUnitData unit : getUnits())
{
((MXMLUnitData)unit).verify();
}
return true;
}
// For debugging only.
void dumpUnits()
{
for (IMXMLUnitData unit : getUnits())
{
System.out.println(((MXMLUnitData)unit).toDumpString());
}
}
/**
* For debugging only.
*/
@Override
public String toString()
{
StringBuffer sb = new StringBuffer();
IMXMLUnitData[] units = getUnits();
int n = units.length;
for (int i = 0; i < n; i++)
{
// Display the unit's index as, for example, [3].
sb.append('[');
sb.append(i);
sb.append(']');
sb.append(' ');
// Display the unit's information.
sb.append(units[i].toString());
sb.append('\n');
}
return sb.toString();
}
public static void main(String[] args)
{
final FileSpecification fileSpec = new FileSpecification(args[0]);
final MXMLTokenizer tokenizer = new MXMLTokenizer(fileSpec);
List<MXMLToken> tokens = null;
try
{
tokens = tokenizer.parseTokens(fileSpec.createReader());
// Build tags and attributes from the tokens.
final MXMLData mxmlData = new MXMLData(tokens, tokenizer.getPrefixMap(), fileSpec);
mxmlData.dumpUnits();
}
catch (FileNotFoundException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
finally
{
IOUtils.closeQuietly(tokenizer);
}
}
}