/* * * 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.projects; import static com.google.common.base.Preconditions.checkNotNull; import java.io.File; import java.io.FilenameFilter; import java.lang.ref.SoftReference; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOCase; import org.apache.commons.io.filefilter.FileFilterUtils; import org.apache.flex.compiler.internal.caches.CacheStoreKeyBase; import org.apache.flex.compiler.internal.caches.SWFCache; import org.apache.flex.compiler.internal.units.ResourceBundleCompilationUnit; import org.apache.flex.compiler.internal.units.SWCCompilationUnit; import org.apache.flex.compiler.problems.DuplicateSourceFileProblem; import org.apache.flex.compiler.problems.ICompilerProblem; import org.apache.flex.compiler.projects.IFlexProject; import org.apache.flex.compiler.units.ICompilationUnit; import org.apache.flex.swc.ISWC; import org.apache.flex.swc.ISWCFileEntry; import org.apache.flex.swc.ISWCLibrary; import org.apache.flex.swc.ISWCManager; import org.apache.flex.swc.ISWCScript; import org.apache.flex.swc.SWCManager; import org.apache.flex.swf.ITagContainer; import org.apache.flex.utils.FileID; import org.apache.flex.utils.FilenameNormalization; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; /** * Manages the library path of a {@link ASProject}. It has a list of SWC file * handles discovered on the library path. The manager doesn't keep SWC models, * because the {@link SWCManager} keep them as {@link SoftReference}. * {@link SWCManager} is responsible for providing a {@link ISWC} model from a * file. */ public final class LibraryPathManager { public static final String SWC_EXT = "swc"; public static final String ANE_EXT = "ane"; // SWC file extension public static final String DOT_SWC_EXT = "." + SWC_EXT; // ANE file extension public static final String DOT_ANE_EXT = "." + ANE_EXT; // A filename filter that only accepts SWC files and ANE files. private static final FilenameFilter SWC_FILTER = FileFilterUtils.or(FileFilterUtils.suffixFileFilter(DOT_SWC_EXT, IOCase.INSENSITIVE), FileFilterUtils.suffixFileFilter(DOT_ANE_EXT, IOCase.INSENSITIVE)); public static boolean isSWCFile(File file) { return SWC_FILTER.accept(file.getParentFile(), file.getName()); } /** * Find all the SWC files on the library path. The given library path * entries can be a SWC file or a directory containing SWC files. Note that * it does <b>NOT</b> read the directories recursively. * * Returns FileID objects so we can be sure that we have not accidenly made a case sensitive comparison */ public static Set<FileID> discoverSWCFilePaths(final File[] libraryPath) { final Set<FileID> swcFiles = new LinkedHashSet<FileID>(); for (final File path : libraryPath) { if (path.isDirectory()) { final File[] swcsInFolder = path.listFiles(SWC_FILTER); for (final File file : swcsInFolder) swcFiles.add( new FileID(file)); } else { swcFiles.add(new FileID(path)); } } return swcFiles; } /** * like discoverSWCFilePaths, but returns File instead of FileID. Only needed for extrenal clients * who don't / shouldn't have access to FileID * @param libraryPath An array of {@code File} objects. */ public static Set<File> discoverSWCFilePathsAsFiles(final File[] libraryPath) { Set<File> ret = new HashSet<File>(); Set<FileID> fileIds = discoverSWCFilePaths( libraryPath); for (FileID f : fileIds) { ret.add(f.getFile()); } return ret; } /** * Create a library path manager for a given project. * * @param flashProject flash project */ public LibraryPathManager(final ASProject flashProject) { checkNotNull(flashProject, "Flash project cannot be null."); this.flashProject = flashProject; this.libraryFilePaths = new LinkedHashMap<String, String>(); } /** Owner project. */ private final ASProject flashProject; /** * The bidirectional map stores normalized paths and source paths of the SWC * files on library path. * <p> * The key is the SWC file path; The value is the source file directory. */ private final Map<String, String> libraryFilePaths; /** * Create {@code SWCCompilationUnit} objects from the given SWC files. These * compilation units will be added to the project. * * @param swcFilePaths an array of SWC file paths * @return new compilation units from the given SWC file paths */ private List<ICompilationUnit> computeUnitsToAdd(final Collection<String> swcFilePaths) { int order = 0; final List<ICompilationUnit> result = new LinkedList<ICompilationUnit>(); final ISWCManager swcManager = flashProject.getWorkspace().getSWCManager(); for (final String swcFilePath : swcFilePaths) { // it is possible for the SWC to not exist on disk, if this method // is being called as part of a SWC file file removal invalidation. File swcFile = new File(swcFilePath); final ISWC swc = swcManager.get(swcFile); computeUnitsToAdd(swc, order, result); order++; } return result; } /** * Create {@code SWCCompilationUnit} objects from the given ISWC. These * compilation units will be added to the project. * * @param swc ISWC * @param order The oder of the SWC in the project * @param cus New compilation units from the given SWC file paths */ private void computeUnitsToAdd(ISWC swc, int order, List<ICompilationUnit> cus) { for (final ISWCLibrary library : swc.getLibraries()) { for (final ISWCScript script : library.getScripts()) { // Multiple definition in a script share the same compilation unit // with the same ABC byte code block. final List<String> qnames = new ArrayList<String>(script.getDefinitions().size()); for (final String definitionQName : script.getDefinitions()) { qnames.add(definitionQName.replace(":", ".")); } final ICompilationUnit cu = new SWCCompilationUnit( flashProject, swc, library, script, qnames, order); cus.add(cu); } } //If the project's locale is empty, then don't try to create ResourceBundleCompilationUnits // because no resource bundles won't go into swf or swc anyways. if (flashProject instanceof IFlexProject && ((IFlexProject)flashProject).getLocales().size() > 0) { //Create compilation units for all the .properties files in the swc. for (final ISWCFileEntry entry : swc.getFiles().values()) { if (FilenameUtils.getExtension(entry.getPath()).equals(ResourceBundleSourceFileHandler.EXTENSION)) { final ICompilationUnit cu = createResourceBundleCompilationUnit(swc, entry); if (cu != null) { cus.add(cu); } } } } } /** * Set the library path. It will populate {@link SWCManager} with SWC files * found on the library path. It also creates {@link SWCCompilationUnit} * objects for the discovered SWC files, and add these compilation units to * the project. * <p> * It is optimized to do nothing when the new library path is same as the * existing setting. * * See setLibrarySourcePath() as to why discoverSWCFilePaths() just ignores * invalid entries in the path * * @param libraryPath the {@code File} on the library path can be a SWC file * or a directory containing SWC files. */ public void setLibraryPath(final File libraryPath[]) { assert libraryPath != null : "Library path cannot be null"; final Set<FileID> swcFilePaths = discoverSWCFilePaths(libraryPath); // compute SWC files to add and remove final Collection<String> swcFilesToAdd = new LinkedHashSet<String>(); final Collection<String> swcFilesToRemove = new LinkedHashSet<String>(); // Now we can convert the FileID's to Strings, since we have eliminated dupes that use // mixed case. From here on, we don't have to worry about upper/lower case. Set<String> swcFilePathStrings = new LinkedHashSet<String>(); for (FileID f : swcFilePaths) swcFilePathStrings.add(f.getFile().getAbsolutePath()); computeAddRemoveSet(swcFilePathStrings, swcFilesToAdd, swcFilesToRemove); // update the library path map libraryFilePaths.clear(); for (final FileID path : swcFilePaths) { libraryFilePaths.put(path.getFile().getAbsolutePath(), null); } // apply changes to the project updateLibraryPath(swcFilesToAdd, swcFilesToRemove); } /** * Set the source path of a SWC library. * * @param library SWC file path * @param sourceDir source directory * * Note if we are passed a bad path here, we just ignore it. This happens often when * Projects have stale/strange lib source paths. * We COULD try to log a problem, but Bruce and Chris discussed this, and it seems like * overkill, as this only affects code hinting. */ public void setLibrarySourcePath(final File library, final File sourceDir) { if (library==null) return; final String swcPath = FilenameNormalization.normalize(library.getPath()); if (sourceDir == null) { // clear source path libraryFilePaths.put(swcPath, null); } else { if (!sourceDir.exists() || !sourceDir.isDirectory() || !libraryFilePaths.containsKey(swcPath) ) return; // set source path final String sourcePath = FilenameNormalization.normalize(sourceDir.getPath()); libraryFilePaths.put(swcPath, sourcePath); } } /** * Find the directory were a given library might live. * Normally used for finding source attachments. * @param libraryFilename name of a library we want to find * @return a String with the full path to the folder. */ public String getAttachedSourceDirectory(String libraryFilename) { final String swcPath = FilenameNormalization.normalize(libraryFilename); final String attachedSourceDirectory = libraryFilePaths.get(swcPath); return attachedSourceDirectory; } /** * Finds a source file in the source directory * for the specified qualified name. * @param sourceDirectory Path to directory where source attachment directory should be exist. * @param qualifiedName Dotted qualified name to find a source file for. * @return Absolute file name of for specified dotted qualified name in the specified library file. */ public static String getAttachedSourceFilename(String sourceDirectory, String qualifiedName) { if (sourceDirectory == null) return null; // Infer source file path based on QName using Flex source path convention. final String relativePath = qualifiedName.replace('.', File.separatorChar); // Hard code to known list of extensions. If we need to get this from project we could // always pass in the extension list, but that seems unnecessary. We DON'T want to force callers // to pass in a project String[] knownExtensions = { "mxml", "as", "fxg" }; for (String ext : knownExtensions) { final File f = new File(sourceDirectory, relativePath.concat(".").concat(ext)); // TODO: there is a potential bug here on case sensitive file systems // as on the fs there could be foo.AS, but we only check the existence // of foo.as, as getHandledFileExtensions() returns lower cases strings. // Also, if there is foo.AS and foo.as, which one do we choose? if (f.exists()) return FilenameNormalization.normalize(f).getAbsolutePath(); } return null; } /** * Get source path for a SWC library. * * @param library SWC file * @return Directory that contains the source for a SWC library; Null if the * source path hasn't been set. */ public File getLibrarySourcePath(final File library) { final String swcPath = FilenameNormalization.normalize(library.getPath()); final String sourcePath = libraryFilePaths.get(swcPath); if (sourcePath == null) return null; else return new File(sourcePath); } /** * Compute the set of libraries to add and remove.<br> * Remove all the existing libraries that are in the new library path.<br> * Add all the new library path that are not in the existing library path. * * @param swcFilePaths Set of swcs to load. The iterator will iterate over * the swcs in priority order. * @param swcFilesToAdd SWC files to be added * @param swcFilesToRemove SWD files to be removed */ private void computeAddRemoveSet(final Set<String> swcFilePaths, final Collection<String> swcFilesToAdd, final Collection<String> swcFilesToRemove) { // only add a SWC to the swcFilesToAdd if it exists in on disk. swcFilePaths // can have files which don't exist yet, as a library project we depend on // may not have been compiled yet. for (String swcPath : swcFilePaths) { File f = new File(swcPath); if (f.exists()) swcFilesToAdd.add(swcPath); } Iterator<String> swcFilePathsIter = swcFilePaths.iterator(); boolean swcOrderDiffers = false; // if an existing swc is not in the newPath, it needs to be removed. for (final String existingSWCFilePath : libraryFilePaths.keySet()) { /* * If the order of the SWCs is different then add and remove all * SWCs in the list after and including that SWC. */ String newSWCFilePath = null; if (!swcOrderDiffers) { if (!swcFilePathsIter.hasNext()) { // we can run past the swcFilePaths when removing the last // element from the library path swcOrderDiffers = true; } else { newSWCFilePath = swcFilePathsIter.next(); if (existingSWCFilePath.equals(newSWCFilePath)) { swcFilesToAdd.remove(existingSWCFilePath); continue; } else { swcOrderDiffers = true; } } } /* * If the SWC order is detected as being different, then * always remove existing SWCs. Remove SWCs from the "add" * list that are no longer needed. */ if (swcOrderDiffers) { swcFilesToRemove.add(existingSWCFilePath); if (!swcFilesToAdd.contains(existingSWCFilePath)) swcFilesToAdd.remove(existingSWCFilePath); } } } /** * Add and remove SWC file on the {@link ASProject}. This method will add * and remove the corresponding {@link ISWC} object as well. * * @param swcFilesToAdd * @param swcFilesToRemove * @param isExternal */ private void updateLibraryPath(final Collection<String> swcFilesToAdd, final Collection<String> swcFilesToRemove) { final List<ICompilationUnit> unitsToAdd = computeUnitsToAdd(swcFilesToAdd); final List<ICompilationUnit> unitsToRemove = computeUnitsToRemove(swcFilesToRemove); assert unitsToAdd != null; assert unitsToRemove != null; // update the project flashProject.updateCompilationUnitsForPathChange( unitsToRemove, unitsToAdd); } /** * Find all the compilation units related to the given SWC file paths. These * compilation units will be removed from the project. This method also * removes the {@code ISWC} objects from the {@link SWCManager}. * * @param swcFilesToRemove SWC file paths * @return compilation units associated with the SWC files */ private List<ICompilationUnit> computeUnitsToRemove(final Collection<String> swcFilesToRemove) { final List<ICompilationUnit> unitsToRemove; unitsToRemove = new ArrayList<ICompilationUnit>(); for (final String swcFilePath : swcFilesToRemove) { final Collection<ICompilationUnit> compilationUnits = flashProject.getCompilationUnits(swcFilePath); // only add SWC compilation units. It's possible to get other compilation unit types here when // looking up by filename, as am EmbedCompilationUnit can have a SWC filename List<ICompilationUnit> swcCompilationUnits = new ArrayList<ICompilationUnit>(compilationUnits.size()); for (ICompilationUnit cu : compilationUnits) { switch (cu.getCompilationUnitType()) { case SWC_UNIT: case RESOURCE_UNIT: swcCompilationUnits.add(cu); break; } } unitsToRemove.addAll(swcCompilationUnits); } return unitsToRemove; } /** * Get {@link ISWC} objects for the SWC files on the library path. * {@code LibraryPathManager} does not keep a list of all the SWC model. * * @return A list of {@link ISWC}'s on the library path. */ protected ImmutableList<ISWC> getLibrarySWCs() { final ISWCManager swcManager = flashProject.getWorkspace().getSWCManager(); final ImmutableList.Builder<ISWC> builder = new ImmutableList.Builder<ISWC>(); for (final String libraryPath : libraryFilePaths.keySet()) { final File swcFile = new File(libraryPath); if (swcFile.exists()) { final ISWC swc = swcManager.get(swcFile); builder.add(swc); // ImmutableList throws exception on null element. } } return builder.build(); } /** * Invalidate a collection of SWC files on the library path. * * @param swcFiles SWC files on the library path * @return true if a library referenced by the project was invalidated */ protected boolean invalidate(Collection<File> swcFiles) { Set<String> swcFilePaths = new HashSet<String>(swcFiles.size()); for (File swcFile : swcFiles) { if (swcFile != null) { String swcFilePath = FilenameNormalization.normalize(swcFile.getPath()); if (libraryFilePaths.containsKey(swcFilePath)) { swcFilePaths.add(swcFilePath); } } } if (swcFilePaths.isEmpty()) return false; updateLibraryPath(swcFilePaths, swcFilePaths); return true; } /** * Invalidate a SWC on the library path. * * @param swc ISWC to invalidate */ protected void invalidate(ISWC swc) { String swcFilename = swc.getSWCFile().getAbsolutePath(); final Collection<ICompilationUnit> unitsToRemove = flashProject.getCompilationUnits(swcFilename); final List<ICompilationUnit> unitsToAdd = new LinkedList<ICompilationUnit>(); computeUnitsToAdd(swc, 0, unitsToAdd); // update any depending projects on this library change flashProject.getWorkspace().swcChanged(unitsToRemove, unitsToAdd, new Runnable() { @Override public final void run() { // update the project flashProject.updateCompilationUnitsForPathChange(unitsToRemove, unitsToAdd); } }); } /** * Search the library path for file entries matching the filename * @param filename Filename to search for * @return ISWCFileEntry or null if not found */ public ISWCFileEntry getFileEntryFromLibraryPath(String filename) { ImmutableList<ISWC> swcs = getLibrarySWCs(); ISWCFileEntry fileEntry = null; for (ISWC swc : swcs) { fileEntry = swc.getFile(filename); if (fileEntry != null) break; } return fileEntry; } /** * Computes the qualified name of a properties file that is in a swc and * creates a compilation unit for that. * * Example: For a properties file located at * "locale/en_US/foo/bar/core.properties" in a swc, the qualified name is * computed as "foo.bar.core". * * @param swc swc that contains the properties file * @param fileEntry file entry that points to the properties file in the swc * @return {@link ResourceBundleCompilationUnit} for the specified swc file entry. */ private ICompilationUnit createResourceBundleCompilationUnit(ISWC swc, ISWCFileEntry fileEntry) { String path = FilenameUtils.separatorsToSystem(fileEntry.getPath()); String[] segments = Iterables.toArray(Splitter.on(File.separator).split(path), String.class); //the pattern used in SWC files to store resource bundles is 'locale/{locale}/..", //therefore the segment count needs to be at least 2. if (segments.length > 2 && ResourceBundleCompilationUnit.LOCALE.equals(segments[0])) { StringBuilder qName = new StringBuilder(); for(int i=2; i<segments.length-1; i++) { qName.append(segments[i]); qName.append('.'); } qName.append(FilenameUtils.getBaseName(segments[segments.length-1])); return new ResourceBundleCompilationUnit( flashProject, fileEntry, qName.toString(), segments[1]); } return null; } /** * Add {@link ICompilerProblem}'s found in the current library path to the * specified collection. * <p> * These problems are with the library path itself, not with the libraries * themselves. For example if a SWC has been deleted and no longer exists. * {@link DuplicateSourceFileProblem} problems. */ void collectProblems(Collection<ICompilerProblem> problems) { final ISWCManager swcManager = flashProject.getWorkspace().getSWCManager(); for (String swcPath : libraryFilePaths.keySet()) { final File swcFile = new File(swcPath); ISWC swc = swcManager.get(swcFile); problems.addAll(swc.getProblems()); for (ISWCLibrary library : swc.getLibraries()) { final CacheStoreKeyBase key = SWFCache.createKey(swc, library.getPath()); final ITagContainer tags = swcManager.getSWFCache().get(key); problems.addAll(tags.getProblems()); } } } /** * For debugging only. */ @Override public String toString() { StringBuilder sb = new StringBuilder(); for (String libraryFilePath : libraryFilePaths.keySet().toArray(new String[0])) { sb.append(libraryFilePath); sb.append('\n'); } return sb.toString(); } }