/* * * 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.flex.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.flex.compiler.common.MutablePrefixMap; import org.apache.flex.compiler.common.PrefixMap; import org.apache.flex.compiler.filespecs.FileSpecification; import org.apache.flex.compiler.filespecs.IFileSpecification; import org.apache.flex.compiler.internal.parsing.mxml.BalancingMXMLProcessor; import org.apache.flex.compiler.internal.parsing.mxml.MXMLToken; import org.apache.flex.compiler.internal.parsing.mxml.MXMLTokenizer; import org.apache.flex.compiler.internal.parsing.mxml.MXMLUnitDataIterator; import org.apache.flex.compiler.mxml.IMXMLData; import org.apache.flex.compiler.mxml.IMXMLTagAttributeData; import org.apache.flex.compiler.mxml.IMXMLTagData; import org.apache.flex.compiler.mxml.IMXMLUnitData; import org.apache.flex.compiler.parsing.MXMLTokenTypes; import org.apache.flex.compiler.problems.ICompilerProblem; import org.apache.flex.compiler.problems.SyntaxProblem; import org.apache.flex.utils.FastStack; import org.apache.flex.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 Falcon we have endeavored to preserve the existing * definition of "nearest", so that for a given MXML file, falcon 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-falcon) 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); } } }