/* * * 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.File; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import javax.xml.parsers.DocumentBuilderFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.apache.flex.compiler.mxml.IMXMLManifestManager; import org.apache.flex.compiler.mxml.IMXMLNamespaceMapping; import org.apache.flex.compiler.problems.ICompilerProblem; import org.apache.flex.compiler.problems.ManifestProblem; import org.apache.flex.compiler.common.XMLName; import org.apache.flex.compiler.filespecs.IFileSpecification; import org.apache.flex.compiler.internal.projects.FlexProject; import org.apache.flex.swc.ISWCComponent; import org.apache.flex.swc.ISWC; import com.google.common.collect.HashMultimap; import com.google.common.collect.SetMultimap; /** * Each {@code FlexProject} has an {@code MXMLManifestManager} to resolve MXML tags to ActionScript classes, * using the <component> tags inside SWCs' catalog.xml files and any manifest files associated * with the project. * This manager must be recreated whenever the library path, or a manifest file, changes. */ public class MXMLManifestManager implements IMXMLManifestManager { /** * Helper method to get the class name from the * class info. Takes are of checking if the class * info is null. * * @param classInfo may be null. * @return The name of the class if classInfo is not null, * null otherwise. */ static String getClassName(ClassInfo classInfo) { return classInfo != null ? classInfo.className : null; } /** * Constructor. * * @param project The {@code FlexProject} for which this manifest manager * provides MXML-tag-to-ActionScript-classname mappings. */ public MXMLManifestManager(FlexProject project) { // Loop over all the SWCs on the library path. for (ISWC swc : project.getLibraries()) { addSWC(swc); } // Loop over all the manifest files that MXML namespace URIs are mapped to. for (IMXMLNamespaceMapping namespaceMapping : project.getNamespaceMappings()) { addManifest(project, namespaceMapping.getURI(), namespaceMapping.getManifestFileName()); } } // Maps an MXML tag name to a fully-qualified classname // such as "spark.components.Button"; null values in this map // indicate that there were inconsistent manifest entries // for the tag name. private Map<XMLName, ClassInfo> lookupMap = new HashMap<XMLName, ClassInfo>(); // Maps a tag name to a fully-qualified class name. This map only contains // manifest entries where 'lookupOnly' is true. This is only really needed // for manifests specified in the -include-namespace option. private Map<XMLName, String> lookupOnlyMap = new HashMap<XMLName, String>(); /** * Maps a fully qualified classname such as "spark.components.Button" to * an MXML tag name such as "<s:Button>". */ private SetMultimap<String, XMLName> reverseLookupMap = HashMultimap.<String, XMLName>create(); // Maps an MXML tag name to a list of (qname, path) duples, // for reporting inconsistencies or duplications between manifests. private HashMap<XMLName, ArrayList<ProblemEntry>> problemMap = new HashMap<XMLName, ArrayList<ProblemEntry>>(); // // Object overrides // /** * For debugging only. * Lists all of the MXML-tag-to-ActionScript-classname mappings, * in sorted order. Useful for debugging manifest problems. */ @Override public String toString() { StringBuilder sb = new StringBuilder(); TreeSet<XMLName> keys = new TreeSet<XMLName>(lookupMap.keySet()); for (XMLName key : keys) { sb.append(key); sb.append(" -> "); sb.append(lookupMap.get(key)); sb.append(", lookupOnly = "); sb.append(isLookupOnly(key)); sb.append('\n'); } return sb.toString(); } // // IMXMLManifestManager implementations // @Override public String resolve(XMLName tagName) { return getClassName(lookupMap.get(tagName)); } @Override public boolean isLookupOnly(XMLName tagName) { return lookupOnlyMap.get(tagName) != null; } @Override public Collection<XMLName> getTagNamesForClass(String className) { Collection<XMLName> result = reverseLookupMap.get(className); if (result == null) return Collections.emptySet(); else return Collections.unmodifiableCollection(result); } @Override public Collection<String> getQualifiedNamesForNamespaces(Set<String> namespaceURIs, boolean manifestEntriesOnly) { HashSet<String> qualifiedNames = new HashSet<String>(); for (Map.Entry<XMLName, ClassInfo> entry : lookupMap.entrySet()) { if (namespaceURIs.contains(entry.getKey().getXMLNamespace())) { ClassInfo classInfo = entry.getValue(); if (classInfo != null && (!manifestEntriesOnly || (manifestEntriesOnly && classInfo.fromManifest))) { qualifiedNames.add(classInfo.className); } } } return qualifiedNames; } // // Other methods // private void addSWC(ISWC swc) { File swcFile = swc.getSWCFile(); // Loop over all the <component> tags in the catalog.xml file // inside each SWC. for (ISWCComponent component : swc.getComponents()) { String uri = component.getURI(); String name = component.getName(); XMLName tagName = new XMLName(uri, name); String qname = component.getQName(); // Add the mapping info in the <component> tag // to the maps of this manifest manager. add(tagName, qname, swcFile.getAbsolutePath(), false); } } private void addManifest(FlexProject project, String uri, String manifestFileName) { Document manifestDocument = null; IFileSpecification manifestFileSpec = project.getWorkspace().getFileSpecification(manifestFileName); try { DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); documentBuilderFactory.setIgnoringElementContentWhitespace(true); documentBuilderFactory.setCoalescing(true); documentBuilderFactory.setIgnoringComments(true); manifestDocument = documentBuilderFactory.newDocumentBuilder().parse(new InputSource(manifestFileSpec.createReader())); } catch (Exception e) { // TODO Report a problem. } if (manifestDocument != null) { NodeList components = manifestDocument.getElementsByTagName("component"); for (int i = 0; i < components.getLength(); i++) { Element component = (Element)components.item(i); if (component != null) { String id = component.getAttribute("id"); if (id != null) { // TODO Why are we checking for dots in the tag name? int lastDot = id.lastIndexOf("."); if (lastDot != -1) id = id.substring(lastDot + 1); } XMLName tagName = new XMLName(uri, id); String className = component.getAttribute("class"); if (className != null) className = className.replaceAll("/", "."); String lookupOnlyStr = component.getAttribute("lookupOnly"); boolean lookupOnly = lookupOnlyStr == null ? false : Boolean.valueOf(lookupOnlyStr).booleanValue(); if (id != null && className != null) { add(tagName, className, manifestFileName, true); if (lookupOnly) addLookupOnly(tagName, className); } } } } else System.out.println("Unable to parse " + manifestFileName); } /** * Adds a mapping to this manifest manager. * * @param tagName An {@code XMLName} for an MXML tag. * * @param className The fully-qualified ActionScript classname * to which this tag maps. * * @param file The SWC file in which the mapping was declared. */ private void add(XMLName tagName, String className, String fileName, boolean fromManifest) { if (!lookupMap.containsKey(tagName)) { // The first manifest entry associating a className // with this tagName is being added. // The ClassInfo keeps track of whether it came // from the catalog of a SWC or from a manifest file. lookupMap.put(tagName, new ClassInfo(className, fromManifest)); reverseLookupMap.put(className, tagName); return; } else { // A particular mapping might come first from a SWC and later // from a manifest. In that case, change the fromManifest flag to true; // otherwise getQualifiedNamesForNamespaces() won't return the // right names and COMPC won't link in all the classes that // were in manifests. if (fromManifest) lookupMap.get(tagName).fromManifest = true; } // If subsequent classNames added for this tagName aren't consistent, // null out the className in this map so that the tag won't // resolve to a class. String oldClassName = getClassName(lookupMap.get(tagName)); if (className.equals(oldClassName)) return; lookupMap.put(tagName, null); reverseLookupMap.remove(oldClassName, tagName); // ProblemEntry entry = new ProblemEntry(className, fileName); ArrayList<ProblemEntry> list = problemMap.get(tagName); if (list == null) { list = new ArrayList<ProblemEntry>(); problemMap.put(tagName, list); } list.add(entry); } /** * Adds a 'lookupOnly' mapping to this manifest manager. * * @param tagName An {@code XMLName} for an MXML tag. * * @param className The fully-qualified ActionScript classname * to which this tag maps. */ private void addLookupOnly(XMLName tagName, String className) { if (!lookupOnlyMap.containsKey(tagName)) { // The first manifest entry associating a className // with this tagName is being added. lookupOnlyMap.put(tagName, className); } } /** * Looks for inconsistent manifest mappings and returns * a collection of compiler problems for them. * * @return A collection of {@code ICompilerProblem} objects. */ public Collection<ICompilerProblem> getProblems() { Collection<ICompilerProblem> problems = new HashSet<ICompilerProblem>(); // Search the lookupMap for null values, which indicate // an inconsistent tagName->className mapping. for (XMLName key : lookupMap.keySet()) { if (lookupMap.get(key) == null) { // The corresponding entry in the problemMap // has information about all the mapping of that tagName. List<ProblemEntry> list = problemMap.get(key); ICompilerProblem problem = new ManifestProblem(list); problems.add(problem); } } return problems; } /** * This inner class stores information about a class in a namespace mapping. */ private static class ClassInfo { /** * Constructor. * * @param className fully qualified class name. * @param fromManifest true if the class name came from a manifest * file entry, false otherwise. */ ClassInfo(String className, boolean fromManifest) { this.className = className; this.fromManifest = fromManifest; } public String className; public boolean fromManifest; } /** * This inner class is a simple duple struct used to keep track * of all the manifest mappings for a particular tag. * For example, <whatever:Foo> might map to a.b.Foo in * X.swc and Y.swc but c.d.Foo in Z.swc. * We keep track of all of this so that we can create compiler * problems describing where the inconsistencies are. */ private static class ProblemEntry { ProblemEntry(String className, String fileName) { this.className = className; this.fileName = fileName; } @SuppressWarnings("unused") public String className; @SuppressWarnings("unused") public String fileName; } }