/* * * 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 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.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.io.FilenameUtils; import org.apache.flex.compiler.problems.DuplicateQNameInSourcePathProblem; import org.apache.flex.compiler.problems.DuplicateSourceFileProblem; import org.apache.flex.compiler.problems.ICompilerProblem; import org.apache.flex.compiler.problems.NonDirectoryInSourcePathProblem; import org.apache.flex.compiler.problems.OverlappingSourcePathProblem; import org.apache.flex.compiler.problems.SourcePathNotFoundProblem; import org.apache.flex.compiler.problems.UnableToListFilesProblem; import org.apache.flex.compiler.projects.IASProject; import org.apache.flex.compiler.projects.IFlexProject; import org.apache.flex.compiler.units.ICompilationUnit; import org.apache.flex.utils.DirectoryID; import org.apache.flex.utils.FilenameNormalization; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Objects; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; /** * Maintains a source path for a {@link ASProject}. */ public final class SourcePathManager { /** * Constructor */ public SourcePathManager(ASProject compilerProject) { this.compilerProject = compilerProject; sourcePaths = new LinkedHashMap<DirectoryID, HashSet<QNameFile>>(); problems = Collections.emptyList(); } private final ASProject compilerProject; // map where we keep all the paths and the qnames/files they define. // use DirectoryID as the key so we can avoid two "identical" paths // that only differ in case (upper vs lower) private LinkedHashMap<DirectoryID, HashSet<QNameFile>> sourcePaths; private Collection<ICompilerProblem> problems; private Collection<ICompilerProblem> duplicateQNameProblems; /** * Test if a file is on the source path or not. * * @param file The file to test. May not be null. * @return true if the file is on the source path, false otherwise. */ public boolean isFileOnSourcePath(File file) { for (final DirectoryID directory : sourcePaths.keySet()) { if (directory.isParentOf(file)) return true; } return false; } private void accumulateQNameFiles(Set<QNameFile> qNameFiles, File directory, String baseQName, String locale, Collection<ICompilerProblem> problems, int order) { assert directory.isDirectory(); assert directory.equals(FilenameNormalization.normalize(directory)); File[] files = directory.listFiles(); if (files == null) { problems.add(new UnableToListFilesProblem(directory)); return; } for (final File file : directory.listFiles()) { assert file.equals(FilenameNormalization.normalize(file)); if (file.isDirectory()) { accumulateQNameFiles(qNameFiles, file, baseQName + file.getName() + ".", locale, problems, order); } else if (compilerProject.getSourceCompilationUnitFactory().canCreateCompilationUnit(file)) { String className = FilenameUtils.getBaseName(file.getName()); String qName = baseQName + className; qNameFiles.add(new QNameFile(qName, file, locale, order)); } } } public static String computeQName(File ancestor, File descendent) { assert ancestor.equals(FilenameNormalization.normalize(ancestor)); assert descendent.equals(FilenameNormalization.normalize(descendent)); StringBuilder result = new StringBuilder(); File current = descendent.getParentFile(); result.insert(0, FilenameUtils.getBaseName(descendent.getPath())); while (current != null) { if (current.equals(ancestor)) return result.toString(); result.insert(0, '.'); result.insert(0, FilenameUtils.getBaseName(current.getPath())); current = current.getParentFile(); } return null; } private static boolean arePathsEqual(File[] newPaths, LinkedHashMap<DirectoryID, HashSet<QNameFile>> oldPaths) { if (newPaths.length != oldPaths.size()) return false; int i = 0; for (DirectoryID oldPath : oldPaths.keySet()) { if (!newPaths[i].isDirectory()) return false; // all the old paths are directories. If this isn't then it must not be equal DirectoryID newDir = new DirectoryID( newPaths[i] ); if (!(newDir.equals(oldPath))) return false; i++; } return true; } private static boolean isAncestorOf(File ancestor, File descendent) { return computeQName(ancestor, descendent) != null; } void handleChangedSourcePath(File[] newSourcePath) { // used to check for duplicates and buildup a mapping of which source files // are contained within each sourcePath Collection<ICompilerProblem> problems = new ArrayList<ICompilerProblem>(); LinkedHashMap<DirectoryID, HashSet<QNameFile>> newSourcePaths = new LinkedHashMap<DirectoryID, HashSet<QNameFile>>(); List<QNameFile> newQNameFilesToCreate = new ArrayList<QNameFile>(); int order = 0; for (File sourcePathEntry : newSourcePath) { // Make sure the entry is a directory if (!sourcePathEntry.isDirectory()) { problems.add(new NonDirectoryInSourcePathProblem(sourcePathEntry)); } else { DirectoryID directoryId = new DirectoryID(sourcePathEntry); if (!newSourcePaths.containsKey(directoryId)) { HashSet<QNameFile> filesInPath = new HashSet<QNameFile>(); newSourcePaths.put(directoryId, filesInPath); // Check for overlapping source path entries. for (File descendent : newSourcePath) { if ((sourcePathEntry != descendent) && (isAncestorOf(sourcePathEntry, descendent))) { problems.add(new OverlappingSourcePathProblem(sourcePathEntry, descendent)); } } String locale = null; if (compilerProject instanceof IFlexProject) locale = ((IFlexProject)compilerProject).getResourceLocale(sourcePathEntry.getAbsolutePath()); accumulateQNameFiles(filesInPath, sourcePathEntry, "", locale, problems, order); // if the source path already exists, no need to re-add files which // already exist Set<QNameFile> existingEntriesForSourcePath = Objects.<Set<QNameFile>> firstNonNull(sourcePaths.get(directoryId), Collections.<QNameFile> emptySet()); // Any qname file that is in filesInPath, but not in existingEntriesForSourcePath // is a new qname file that we need to create a compilation unit for. newQNameFilesToCreate.addAll(Sets.difference(filesInPath, existingEntriesForSourcePath)); } } ++order; } // if an existing path is not in the newPaths, it needs to be removed. // work out which compilation units need to be removed as a result of changing Set<ICompilationUnit> unitsToRemove = new HashSet<ICompilationUnit>(); for (Map.Entry<DirectoryID, HashSet<QNameFile>> e : sourcePaths.entrySet()) { Set<QNameFile> newSourcePathFiles = Objects.<Set<QNameFile>>firstNonNull(newSourcePaths.get(e.getKey()), Collections.<QNameFile>emptySet()); Set<QNameFile> filesToRemove = Sets.difference(e.getValue(), newSourcePathFiles); for (QNameFile qNameFile : filesToRemove) { File sourceFile = qNameFile.file; Collection<ICompilationUnit> sourcePathCompilationUnitsToRemove = Collections2.filter(compilerProject.getCompilationUnits(sourceFile.getAbsolutePath()), new Predicate<ICompilationUnit>() { @Override public boolean apply(ICompilationUnit cu) { DefinitionPriority defPriority = (DefinitionPriority)cu.getDefinitionPriority(); return defPriority.getBasePriority() == DefinitionPriority.BasePriority.SOURCE_PATH; }}); unitsToRemove.addAll(sourcePathCompilationUnitsToRemove); } } // set the new sources sourcePaths = newSourcePaths; List<ICompilationUnit> unitsToAdd = new ArrayList<ICompilationUnit>(); if (!newQNameFilesToCreate.isEmpty()) { for (QNameFile qNameFile : newQNameFilesToCreate) { ICompilationUnit newCU = compilerProject.getSourceCompilationUnitFactory().createCompilationUnit( qNameFile.file, DefinitionPriority.BasePriority.SOURCE_PATH, qNameFile.order, qNameFile.qName, qNameFile.locale); //It can be null in some cases, see #ResourceBundleSourceFileHandler if(newCU != null) unitsToAdd.add(newCU); } } this.problems = problems; compilerProject.updateCompilationUnitsForPathChange(unitsToRemove, unitsToAdd); checkForDuplicateQNames(); } private boolean foundAllCompilationUnits() { compilerProject.getWorkspace(); for (Set<QNameFile> qNameFiles : sourcePaths.values()) { for (QNameFile qNameFile : qNameFiles) { if (compilerProject.getWorkspace().getCompilationUnits(qNameFile.file.getAbsolutePath(), compilerProject).isEmpty()) { if (compilerProject.getSourceCompilationUnitFactory().needCompilationUnit(qNameFile.file, qNameFile.qName, qNameFile.locale)) return false; } } } return true; } /** * Updates the list of directories that are the source path. This method may * add or remove {@link ICompilationUnit}'s from the {@link IASProject} * associated with this {@link SourcePathManager}. * * @param newSourcePath */ void setSourcePath(File[] newSourcePath) { newSourcePath = FilenameNormalization.normalize(newSourcePath); try { if (arePathsEqual(newSourcePath, sourcePaths)) return; handleChangedSourcePath(newSourcePath); } finally { assert foundAllCompilationUnits(); } } private Collection<ICompilationUnit> getCompilationUnits(File f) { assert FilenameNormalization.normalize(f).equals(f); return Collections2.filter(compilerProject.getCompilationUnits(f.getAbsolutePath()), new Predicate<ICompilationUnit>() { @Override public boolean apply(ICompilationUnit compilationUnit) { DefinitionPriority priority = ((DefinitionPriority)compilationUnit.getDefinitionPriority()); return priority.getBasePriority() == DefinitionPriority.BasePriority.SOURCE_PATH; } }); } /** * Notifies this {@link SourcePathManager} that a file as been added to the * file system. * <p> * This {@link SourcePathManager} will determine if any source path entries * contain the specified file and if so, will create new * {@link ICompilationUnit}'s and add them to the project. * * @param f File that has been added to the file system. * @return true if any {@link ICompilationUnit}'s were created and added to * the project. */ public boolean addFile(File f) { f = FilenameNormalization.normalize(f); if (!getCompilationUnits(f).isEmpty()) return false; ArrayList<QNameFile> qNameFiles = new ArrayList<QNameFile>(1); int order = 0; for (Map.Entry<DirectoryID, HashSet<QNameFile>> sourcePathEntry : sourcePaths.entrySet()) { DirectoryID dir = sourcePathEntry.getKey(); String qname = computeQName(dir.getFile(), f); if (qname != null) { String locale = null; if(compilerProject instanceof IFlexProject) locale = ((IFlexProject)compilerProject).getResourceLocale(dir.getFile().getAbsolutePath()); QNameFile newQNameFile = new QNameFile(qname, f, locale, order); sourcePathEntry.getValue().add(newQNameFile); qNameFiles.add(newQNameFile); } ++order; } if (qNameFiles.isEmpty()) return false; List<ICompilationUnit> unitsToAdd = new ArrayList<ICompilationUnit>(); for (QNameFile qNameFile : qNameFiles) { ICompilationUnit newCU = compilerProject.getSourceCompilationUnitFactory().createCompilationUnit( qNameFile.file, DefinitionPriority.BasePriority.SOURCE_PATH, qNameFile.order, qNameFile.qName, qNameFile.locale); if (newCU != null) unitsToAdd.add(newCU); } // If none of the files had a file extension we knew how to make // a compilation unit for we might not have any new compilation // units to add to the project. if (unitsToAdd.size() == 0) return false; assert unitsToAdd != null; compilerProject.updateCompilationUnitsForPathChange(Collections.<ICompilationUnit>emptyList(), unitsToAdd); checkForDuplicateQNames(); return true; } private void checkForDuplicateQNames() { Map<String, Set<QNameFile>> qNameMap = new HashMap<String, Set<QNameFile>>(); for (HashSet<QNameFile> qNameFiles : sourcePaths.values()) { for (QNameFile qNameFile : qNameFiles) { Set<QNameFile> qNameFilesForQName = qNameMap.get(qNameFile.qName); if (qNameFilesForQName == null) { qNameFilesForQName = new HashSet<QNameFile>(1); qNameMap.put(qNameFile.qName, qNameFilesForQName); } qNameFilesForQName.add(qNameFile); } } ArrayList<ICompilerProblem> duplicateQNameProblems = new ArrayList<ICompilerProblem>(); for (Map.Entry<String, Set<QNameFile>> qNameMapEntry : qNameMap.entrySet()) { Set<QNameFile> qNameFiles = qNameMapEntry.getValue(); String qName = qNameMapEntry.getKey(); if (qNameFiles.size() > 1) { StringBuilder listString = new StringBuilder(); int found = 0; for (QNameFile qNameFile : qNameFiles) { if(ResourceBundleSourceFileHandler.EXTENSION.equalsIgnoreCase( FilenameUtils.getExtension(qNameFile.file.getAbsolutePath()))) { //TODO: https://bugs.adobe.com/jira/browse/CMP-923 //As of now, we ignore the properties files while //checking the duplicate names until we find a sophisticated way //to this in the future. continue; } if (found++ > 0) listString.append(", "); assert qName.equals(qNameFile.qName); listString.append(qNameFile.file.getAbsolutePath()); } if(found > 1) //if we found more than one duplicate qname then report a problem { ICompilerProblem problem = new DuplicateQNameInSourcePathProblem(listString.toString(), qName); duplicateQNameProblems.add(problem); } } } if (duplicateQNameProblems.size() > 0) this.duplicateQNameProblems = duplicateQNameProblems; else this.duplicateQNameProblems = null; } /** * Notifies this {@link SourcePathManager} that a file as been deleted from * the file system. * <p> * This {@link SourcePathManager} will determine if any source path entries * contain the specified file and if so, will remove * {@link ICompilationUnit}'s from the project. * * @param f File that has been removed from the file system. * @return true if any {@link ICompilationUnit}'s were removed from the * project. */ public boolean removeFile(File f) { Collection<ICompilationUnit> unitsToRemove = getCompilationUnits(f); if (!unitsToRemove.isEmpty()) { List<ICompilationUnit> unitsToAdd = Collections.emptyList(); compilerProject.updateCompilationUnitsForPathChange(unitsToRemove, unitsToAdd); removeQNames(f); // if there are already duplicate names, after the remove, check for duplicates // again, as the remove may have fixed the problem. if (this.duplicateQNameProblems != null && !this.duplicateQNameProblems.isEmpty()) { checkForDuplicateQNames(); } return true; } return false; } private void removeQNames(File f) { for (Map.Entry<DirectoryID, HashSet<QNameFile>> sourcePathEntry : sourcePaths.entrySet()) { DirectoryID dir = sourcePathEntry.getKey(); String qname = computeQName(dir.getFile(), f); if (qname != null) { for (Iterator<QNameFile> iter = sourcePathEntry.getValue().iterator(); iter.hasNext();) { QNameFile qNameFile = iter.next(); if (qNameFile.file.equals(f)) { iter.remove(); } } } } } /** * Add {@link ICompilerProblem}'s found in the current source path to the * specified collection. * <p> * These problems are with the source path itself, not with sources * discovered in the source path. For example the returned collection would * not contain syntax error problems, put will contain * {@link DuplicateSourceFileProblem} problems. */ void collectProblems(Collection<ICompilerProblem> problems) { problems.addAll(this.problems); if (duplicateQNameProblems != null) problems.addAll(duplicateQNameProblems); for (DirectoryID sourcePath : sourcePaths.keySet()) { if (!sourcePath.getFile().exists()) { problems.add(new SourcePathNotFoundProblem(sourcePath.getFile().getAbsolutePath())); } } } /** * Adds all the {@link ICompilationUnit}'s whose root source file is the * specified File to the specified collection. * * @param rootSourceFile File to search for. * @param units Collection to add to. */ public void collectionCompilationUnitsForRootSourceFile(File rootSourceFile, Collection<ICompilationUnit> units) { Collection<ICompilationUnit> compilationUnits = compilerProject.getCompilationUnits(rootSourceFile.getAbsolutePath()); units.addAll(compilationUnits); } /** * Determines of the specified file is the root source file of any * {@link ICompilationUnit} created by this {@link SourcePathManager}. * * @param rootSourceFile File to search for. * @return true if the specified file is the root source file of any * {@link ICompilationUnit}'s created by this {@link SourcePathManager}. */ public boolean hasCompilationUnitsForRootSourceFile(File rootSourceFile) { Collection<ICompilationUnit> compilationUnits = compilerProject.getCompilationUnits(rootSourceFile.getAbsolutePath()); return compilationUnits.size() > 0; } public static class QNameFile { final String qName; final File file; final String locale; final int order; QNameFile(String qName, File file, String locale, int order) { this.qName = qName; this.file = file; this.locale = locale; this.order = order; } @Override public int hashCode() { return qName.hashCode() + file.hashCode(); } @Override public boolean equals(Object other) { if (other == this) return true; if (!(other instanceof QNameFile)) return false; QNameFile otherQNameFile = (QNameFile)other; return qName.equals(otherQNameFile.qName) && file.equals(otherQNameFile.file); } @Override public String toString() { return "QNameFile qName:" + this.qName + " file:" + this.file; } } String getSourceFileFromSourcePath(String file) { String sourceFile = null; for (DirectoryID sourcePath : sourcePaths.keySet()) { sourceFile = getSourceFileInPath(sourcePath.getFile(), file); if (sourceFile != null) break; } return sourceFile; } private List<QNameFile> createQNameFilesForFile(File f) { ArrayList<QNameFile> qNameFiles = new ArrayList<QNameFile>(1); int order = 0; for (Map.Entry<DirectoryID, HashSet<QNameFile>> sourcePathEntry : sourcePaths.entrySet()) { DirectoryID dir = sourcePathEntry.getKey(); String qname = computeQName(dir.getFile(), f); if (qname != null) { String locale = null; if(compilerProject instanceof IFlexProject) locale = ((IFlexProject)compilerProject).getResourceLocale(dir.getFile().getAbsolutePath()); QNameFile newQNameFile = new QNameFile(qname, f, locale, order); sourcePathEntry.getValue().add(newQNameFile); qNameFiles.add(newQNameFile); } ++order; } return qNameFiles; } /** * @param rootSourceFileName The absolute normalized file name for the root * source file of the new {@link QNameFile}. * @return A QNameFile for the rootSourceFileName, or null if it could not be computed */ QNameFile computeQNameForFilename(String rootSourceFileName) { List<QNameFile> qNameFiles = createQNameFilesForFile(new File(rootSourceFileName)); if (qNameFiles.isEmpty()) return null; // If there is more than one qNameFile, just use the first one. return Iterables.getFirst(qNameFiles, null); } /** * @param path Path to search for file in. May be null. * @param file Filename to search for. Can't be null. * @return Full path to file. null if not found */ public static String getSourceFileInPath(File path, String file) { File sourceFile; if (path != null) { sourceFile = new File(path, file); } else { sourceFile = new File(file); } if (sourceFile.exists()) { return FilenameNormalization.normalize(sourceFile.getAbsolutePath()); } return null; } /** * * @return the source path as a list of {@linkplain File}. */ public List<File> getSourcePath() { List<File> paths = new ArrayList<File>(sourcePaths.keySet().size()); for (DirectoryID path : sourcePaths.keySet()) { paths.add(path.getFile()); } return paths; } /** * For debugging only. */ @Override public String toString() { return Joiner.on('\n').join(Iterables.transform(sourcePaths.keySet(), new Function<DirectoryID, String>(){ @Override public String apply(DirectoryID input) { return input.getFile().getAbsolutePath(); }})); } }