blob: da6b74f7217ae8b9a68c7e51aca41e3add95095e [file] [log] [blame]
/*
*
* 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.royale.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.royale.compiler.problems.DuplicateQNameInSourcePathProblem;
import org.apache.royale.compiler.problems.DuplicateSourceFileProblem;
import org.apache.royale.compiler.problems.ICompilerProblem;
import org.apache.royale.compiler.problems.NonDirectoryInSourcePathProblem;
import org.apache.royale.compiler.problems.OverlappingSourcePathProblem;
import org.apache.royale.compiler.problems.SourcePathNotFoundProblem;
import org.apache.royale.compiler.problems.UnableToListFilesProblem;
import org.apache.royale.compiler.projects.IASProject;
import org.apache.royale.compiler.projects.IRoyaleProject;
import org.apache.royale.compiler.units.ICompilationUnit;
import org.apache.royale.utils.DirectoryID;
import org.apache.royale.utils.FilenameNormalization;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
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;
}
public File getSourcePath(File file)
{
for (final DirectoryID directory : sourcePaths.keySet())
{
if (directory.isParentOf(file))
return directory.getFile();
}
return null;
}
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 IRoyaleProject)
locale = ((IRoyaleProject)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 =
MoreObjects.<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 =
MoreObjects.<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;
}
@Override
public boolean test(ICompilationUnit input)
{
return apply(input);
}
});
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;
}
@Override
public boolean test(ICompilationUnit input)
{
return apply(input);
}
});
}
/**
* 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 IRoyaleProject)
locale = ((IRoyaleProject)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 IRoyaleProject)
locale = ((IRoyaleProject)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();
}}));
}
}