| /* |
| * |
| * 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 com.adobe.internal.fxg.sax; |
| |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.Stack; |
| |
| import org.xml.sax.Attributes; |
| import org.xml.sax.Locator; |
| import org.xml.sax.SAXException; |
| import org.xml.sax.helpers.DefaultHandler; |
| |
| import com.adobe.fxg.util.FXGLog; |
| import com.adobe.fxg.util.FXGLogger; |
| import com.adobe.fxg.FXGException; |
| import com.adobe.fxg.FXGConstants; |
| import com.adobe.fxg.dom.FXGNode; |
| |
| import com.adobe.internal.fxg.dom.CDATANode; |
| import com.adobe.internal.fxg.dom.GraphicNode; |
| import com.adobe.internal.fxg.dom.DefinitionNode; |
| import com.adobe.internal.fxg.dom.DelegateNode; |
| import com.adobe.internal.fxg.dom.PreserveWhiteSpaceNode; |
| |
| import static com.adobe.fxg.FXGConstants.*; |
| |
| /** |
| * This SAX2 based scanner converts an FXG document (an XML based description of |
| * a graphical asset) to a simple object graph to serve as an intermediate |
| * representation. The document must be in the FXG 1.0 namespace and the root |
| * element must be a <Graphic> tag. |
| * |
| * @author Peter Farland |
| * @author Sujata Das |
| */ |
| public class FXGSAXScanner extends DefaultHandler |
| { |
| // Namespaces |
| public static final String APACHE_FLEX_NAMESPACE = "http://ns.apache.org/flex/2012"; |
| |
| private static boolean REJECT_MAJOR_VERSION_MISMATCH = false; |
| |
| // A special case needed to short circuit GroupNode creation inside a |
| // Definition as such Groups are not the same as those in the graphics |
| // tree. |
| private static final String FXG_GROUP_DEFINITION_ELEMENT = "[GroupDefinition]"; |
| |
| private String profile; |
| private GraphicNode root; |
| private Stack<FXGNode> stack; |
| private int skippedElementCount; |
| private boolean seenPrivateElement = false; |
| private boolean inMaskAfterPrivateElement = false; |
| private Locator locator; |
| private int startLine = 0; |
| private int startColumn = 0; |
| private String documentName = null; |
| private String unknownElement = null; |
| |
| // FXG version handler to handle different fxg versions |
| // depending on input file version at runtime. |
| private FXGVersionHandler versionHandler = null; |
| |
| /** |
| * Construct a new FXGSAXScanner |
| */ |
| public FXGSAXScanner(String profile) |
| { |
| super(); |
| this.profile = profile; |
| if (profile.equals(FXG_PROFILE_MOBILE)) |
| versionHandler = FXGVersionHandlerRegistry.getDefaultMobileHandler(); |
| else |
| versionHandler = FXGVersionHandlerRegistry.getDefaultHandler(); |
| if (versionHandler == null) |
| throw new FXGException("FXGVersionHandlerNotRegistered", FXGVersionHandlerRegistry.defaultVersion.asDouble()); |
| } |
| |
| /** |
| * Provides access to the root FXGNode of the FXG document AFTER parsing. |
| * |
| * @return the root FXGNode of the DOM. |
| */ |
| public FXGNode getRootNode() |
| { |
| return root; |
| } |
| |
| //-------------------------------------------------------------------------- |
| // |
| // SAX DefaultHandler Implementation |
| // |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void setDocumentLocator(Locator locator) |
| { |
| this.locator = locator; |
| } |
| |
| /** |
| * Set document name used for logging. |
| * |
| * @return the document name |
| */ |
| public String getDocumentName() |
| { |
| return documentName; |
| } |
| |
| /** |
| * Get document name used for logging. |
| * |
| * @param documentName the document name |
| */ |
| public void setDocumentName(String documentName) |
| { |
| this.documentName = documentName; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void startDocument() throws SAXException |
| { |
| stack = new Stack<FXGNode>(); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void startElement(String uri, String localName, String name, |
| Attributes attributes) throws SAXException |
| { |
| // First check if we're currently skipping elements |
| if (isSkippedElement(uri, localName, true)) |
| skippedElementCount++; |
| if (inSkippedElement()) |
| return; |
| |
| // Check if we're currently skipping unknown elements |
| if (unknownElement != null) |
| return; |
| |
| // Record starting position |
| startLine = locator.getLineNumber(); |
| startColumn = locator.getColumnNumber(); |
| |
| // Check the current parent |
| FXGNode parent = null; |
| if (stack.size() > 0) |
| parent = stack.peek(); |
| |
| // Switch to special GroupDefinitionNode for Definition child |
| if (isFXGNamespace(uri)) |
| { |
| if (parent instanceof DefinitionNode && FXG_GROUP_ELEMENT.equals(localName)) |
| localName = FXG_GROUP_DEFINITION_ELEMENT; |
| } |
| |
| // Create a node for this element |
| FXGNode node = createNode(uri, localName); |
| |
| if (node == null) |
| { |
| if (root != null) |
| { |
| if (root.isVersionGreaterThanCompiler()) |
| { |
| // Warning: Minor version of this FXG file is greater than minor |
| // version supported by this compiler. Log a warning for an |
| // unknown element. |
| FXGLog.getLogger().log(FXGLogger.WARN, "UnknownElement", null, documentName, startLine, startColumn, localName, versionHandler.getVersion().asString()); |
| unknownElement = localName; |
| return; |
| }else |
| { |
| throw new FXGException(startLine, startColumn, "UnknownElementInVersion", root.getFileVersion().asString(), localName); |
| } |
| } |
| else |
| { |
| throw new FXGException(startLine, startColumn, "InvalidFXGRootNode"); |
| } |
| } |
| |
| // Provide access to the root document node used for querying version |
| // for non-root elements |
| if (root != null) |
| { |
| node.setDocumentNode(root); |
| } |
| |
| // Set node name if it is a delegate node. This allows proper error |
| // message to be reported. |
| if (node instanceof DelegateNode) |
| { |
| DelegateNode propertyNode = (DelegateNode)node; |
| propertyNode.setName(localName); |
| } |
| |
| // Set attributes on the current node |
| for (int i = 0; i < attributes.getLength(); i++) |
| { |
| String attributeURI = attributes.getURI(i); |
| if (attributeURI == null || attributeURI == "" || |
| isFXGNamespace(attributeURI) || |
| isApacheFlexNamespace(attributeURI)) |
| { |
| String attributeName = attributes.getLocalName(i); |
| String attributeValue = attributes.getValue(i); |
| node.setAttribute(attributeName, attributeValue); |
| } |
| } |
| |
| // Associate child with parent node (and handle any special |
| // relationships) |
| if (parent != null) |
| { |
| if (node instanceof DelegateNode) |
| { |
| DelegateNode propertyNode = (DelegateNode)node; |
| propertyNode.setDelegate(parent); |
| } |
| else |
| { |
| parent.addChild(node); |
| } |
| } |
| else if (node instanceof GraphicNode) |
| { |
| root = (GraphicNode)node; |
| // Provide access to the root document node |
| node.setDocumentNode(root); |
| if (root.getVersion() == null) |
| { |
| // Exception: <Graphic> doesn't have the required attribute |
| // "version". |
| throw new FXGException(startLine, startColumn, "MissingVersionAttribute"); |
| } |
| else |
| { |
| if (!isMajorVersionMatch(root)) |
| { |
| FXGVersionHandler newVHandler = FXGVersionHandlerRegistry.getVersionHandler(root.getVersion()); |
| |
| if (newVHandler == null) |
| { |
| if (REJECT_MAJOR_VERSION_MISMATCH) |
| { |
| // Exception:Major version of this FXG file is greater than |
| // major version supported by this compiler. Cannot process |
| // the file. |
| throw new FXGException(startLine, startColumn, "InvalidFXGVersion", root.getVersion().asString()); |
| } |
| else |
| { |
| // Warning: Major version of this FXG file is greater than |
| // major version supported by this compiler. |
| FXGLog.getLogger().log(FXGLogger.WARN, "MajorVersionMismatch", null, getDocumentName(), startLine, startColumn); |
| |
| //use the latest version handler |
| versionHandler = FXGVersionHandlerRegistry.getLatestVersionHandler(); |
| if (versionHandler == null) |
| { |
| throw new FXGException("FXGVersionHandlerNotRegistered", root.getVersion().asString()); |
| } |
| } |
| } |
| else |
| { |
| versionHandler = newVHandler; |
| } |
| } |
| } |
| // Provide reference to the handler for querying version of the |
| // current document processed. |
| root.setDocumentName(documentName); |
| root.setVersionGreaterThanCompiler(root.getVersion().greaterThan(versionHandler.getVersion())); |
| root.setReservedNodes(versionHandler.getElementNodes(uri)); |
| root.setCompilerVersion(versionHandler.getVersion()); |
| root.setProfile(profile); |
| } |
| else |
| { |
| // Exception:<Graphic> must be the root node of an FXG document. |
| throw new FXGException(startLine, startColumn, "InvalidFXGRootNode"); |
| } |
| |
| stack.push(node); |
| |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void characters(char[] ch, int start, int length) |
| throws SAXException |
| { |
| if (stack != null && stack.size() > 0 && !inSkippedElement() && (unknownElement == null)) |
| { |
| FXGNode node = stack.peek(); |
| String content = new String(ch, start, length); |
| |
| if (!(node instanceof PreserveWhiteSpaceNode)) |
| { |
| content = content.trim(); |
| } |
| |
| if (content.length() > 0) |
| { |
| CDATANode cdata = new CDATANode(); |
| cdata.content = content; |
| assignNodeLocation(cdata); |
| node.addChild(cdata); |
| } |
| } |
| |
| // Reset starting position |
| startLine = locator.getLineNumber(); |
| startColumn = locator.getColumnNumber(); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void endElement(String uri, String localName, String name) |
| throws SAXException |
| { |
| if (isSkippedElement(uri, localName, false)) |
| { |
| skippedElementCount--; |
| } |
| else if (unknownElement != null) |
| { |
| if (unknownElement.equals(localName)) |
| { |
| unknownElement = null; |
| } |
| } |
| else if (!inSkippedElement()) |
| { |
| stack.pop(); |
| } |
| |
| // Reset starting position |
| startLine = locator.getLineNumber(); |
| startColumn = locator.getColumnNumber(); |
| } |
| |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Other Methods |
| // |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * @return the last processed line number |
| */ |
| public int getStartLine() |
| { |
| return startLine; |
| } |
| |
| /** |
| * @return the last processed column number |
| */ |
| public int getStartColumn() |
| { |
| return startColumn; |
| } |
| |
| /** |
| * @param uri - the namespace URI to check |
| * @return whether the given namespace URI is considered an FXG namespace. |
| */ |
| protected boolean isFXGNamespace(String uri) |
| { |
| return FXG_NAMESPACE.equals(uri); |
| } |
| |
| /** |
| * @param uri - the namespace URI to check |
| * @return whether the given namespace URI is considered an Apache Flex namespace. |
| */ |
| protected boolean isApacheFlexNamespace(String uri) |
| { |
| return APACHE_FLEX_NAMESPACE.equals(uri); |
| } |
| |
| /** |
| * Specifies that a particular element should be skipped while scanning for |
| * tokens in an FXG document. All of the element's attributes and child |
| * nodes will be skipped too. |
| * |
| * @param version - the version of the FXG element |
| * @param uri - the namespace URI of the element to skip |
| * @param localName - the name of the element to skip |
| */ |
| protected void skipElement(double version, String uri, String localName) |
| { |
| if (localName == null) |
| return; |
| |
| FXGVersionHandler versionHandler = FXGVersionHandlerRegistry.getVersionHandler(version); |
| if (versionHandler != null) |
| { |
| HashSet<String>skippedElements = new HashSet<String>(1); |
| skippedElements.add(localName); |
| versionHandler.registerSkippedElements(uri, skippedElements); |
| } |
| else |
| { |
| throw new FXGException("FXGVersionHandlerNotRegistered", version); |
| } |
| |
| } |
| |
| |
| /** |
| * Determines whether an element should be skipped. |
| * |
| * @param uri - the namespace URI of the element |
| * @param localName - the name of the element |
| * @return true if the element has been marked as skipped, otherwise false. |
| */ |
| protected boolean isSkippedElement(String uri, String localName, boolean startElement) |
| { |
| Set<String> skippedElements = versionHandler.getSkippedElements(uri); |
| if (skippedElements != null) |
| { |
| if (skippedElements.contains(FXGConstants.FXG_PRIVATE_ELEMENT)) |
| { |
| validatePrivateElement(localName, startElement); |
| } |
| if (skippedElements.contains(localName)) |
| { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| |
| /** |
| * Attempts to construct an instance of FXGNode for the given element. |
| * |
| * @param uri - the namespace URI of the element |
| * @param localName - the name of the element |
| * @return FXGNode instance if |
| */ |
| protected FXGNode createNode(String uri, String localName) |
| { |
| FXGNode node = null; |
| |
| try |
| { |
| Map<String, Class<? extends FXGNode>> elementNodes = getElementNodes(uri); |
| if (elementNodes != null) |
| { |
| Class<? extends FXGNode> nodeClass = elementNodes.get(localName); |
| if (nodeClass != null) |
| { |
| node = (FXGNode)nodeClass.newInstance(); |
| } |
| else if (root != null) |
| { |
| node = root.getDefinitionInstance(localName); |
| } |
| } |
| } |
| catch (Throwable t) |
| { |
| throw new FXGException(startLine, startColumn, "ErrorScanningFXG", t); |
| } |
| |
| if (node != null) |
| { |
| assignNodeLocation(node); |
| } |
| |
| return node; |
| } |
| |
| /** |
| * @return if currently in a skipped element. |
| */ |
| private boolean inSkippedElement() |
| { |
| return skippedElementCount > 0; |
| } |
| |
| /** |
| * Registers a custom FXGNode for a particular type of element encountered |
| * while scanning an FXG document. |
| * |
| * @param version - the version of the FXG element |
| * @param uri - the namespace URI of the FXG element |
| * @param localName - the local name of the FXG element |
| * @param nodeClass - Class of an FXGNode implementation that will represent |
| * an element in the DOM and process its attributes and child nodes during |
| * parsing. |
| */ |
| protected void registerElementNode(double version, String uri, String localName, Class<? extends FXGNode> nodeClass) |
| { |
| FXGVersionHandler vHandler = FXGVersionHandlerRegistry.getVersionHandler(version); |
| if (vHandler != null) |
| { |
| HashMap<String, Class<? extends FXGNode>> elementNodes = new HashMap<String, Class<? extends FXGNode>>(4); |
| elementNodes.put(localName, nodeClass); |
| vHandler.registerElementNodes(uri, elementNodes); |
| } |
| else |
| { |
| throw new FXGException("FXGVersionHandlerNotRegistered", version); |
| } |
| } |
| |
| |
| /** |
| * Record the start and end line and column information for this node. |
| * @param node - the current node |
| */ |
| private void assignNodeLocation(FXGNode node) |
| { |
| if (node != null) |
| { |
| node.setStartLine(startLine); |
| node.setStartColumn(startColumn); |
| node.setEndLine(locator.getLineNumber()); |
| node.setEndColumn(locator.getColumnNumber()); |
| } |
| } |
| |
| /** |
| * @param uri - the namespace URI of the registered FXG elements. |
| * @return a Map of the FXGNode Classes registered for elements in the |
| * given namespace URI. |
| */ |
| private Map<String, Class<? extends FXGNode>> getElementNodes(String uri) |
| { |
| return versionHandler.getElementNodes(uri); |
| } |
| |
| /** |
| * validates restrictions on PRIVATE element |
| * @param localName |
| */ |
| private void validatePrivateElement(String localName, boolean startElement) |
| { |
| if (!startElement) |
| { |
| if (inMaskAfterPrivateElement && localName.equals(FXGConstants.FXG_MASK_ELEMENT)) |
| inMaskAfterPrivateElement = false; |
| return; |
| } |
| |
| if (localName.equals(FXGConstants.FXG_PRIVATE_ELEMENT)) |
| { |
| if (seenPrivateElement) |
| { |
| throw new FXGException("PrivateElementMultipleOccurrences", startLine, startColumn); |
| } |
| else |
| { |
| if ((!inSkippedElement()) && stack.size() == 1) |
| seenPrivateElement = true; |
| else |
| throw new FXGException("PrivateElementNotChildOfGraphic", startLine, startColumn); |
| } |
| } |
| else |
| { |
| if (seenPrivateElement && (!inSkippedElement())) |
| { |
| if ((!inMaskAfterPrivateElement) && (localName.equals(FXGConstants.FXG_MASK_ELEMENT))) |
| { |
| inMaskAfterPrivateElement = true; |
| } |
| else |
| { |
| if (!inMaskAfterPrivateElement) |
| throw new FXGException("PrivateElementNotLast", startLine, startColumn); |
| } |
| } |
| } |
| } |
| |
| /** |
| * @return - true if major version of the FXG file matches the compiler's |
| * major version. false otherwise. |
| */ |
| private boolean isMajorVersionMatch(GraphicNode root) |
| { |
| long majorVersion = root.getVersion().getMajorVersion(); |
| long compilerMajorVersion = versionHandler.getVersion().getMajorVersion(); |
| if (majorVersion == compilerMajorVersion) |
| return true; |
| else |
| return false; |
| } |
| |
| } |