blob: 11e520171858c59d062d7211bc459b3068a8ead8 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2006 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
* Matt Chapman - initial version
* Helen Hawkins - updated for new ajde interface (bug 148190)
*******************************************************************************/
package org.eclipse.ajdt.internal.core.ajde;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import org.aspectj.ajde.core.IOutputLocationManager;
import org.eclipse.ajdt.core.AJLog;
import org.eclipse.ajdt.core.AspectJCorePreferences;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.jdt.core.IClasspathContainer;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.internal.core.JavaModelManager;
import org.eclipse.jdt.internal.core.builder.State;
import org.eclipse.jdt.internal.core.builder.StringSet;
import org.eclipse.jdt.internal.core.util.Util;
/**
* IOutputLocationManager implementation which uses the methods on IJavaProject
* to work out where the output should be sent.
*
* Important note about paths:
* Use Path.toOSString when describing a file on the filesystem
* Use Path.toPortableString when describing a resource in Eclipse's workspace.
*/
public class CoreOutputLocationManager implements IOutputLocationManager {
/**
*
* @author Andrew Eisenberg
* @created Apr 9, 2009
* use this to ensure that the longest strings are looked at first
*
* so, if src and src2 are both source folders, src2 will be
* examined first
*/
static class StringLengthComparator implements Comparator<String> {
public int compare(String s1, String s2) {
if (s1 == null) {
if (s2 == null) {
return 0;
}
return -1;
}
if (s2 == null) {
return 1;
}
int len1 = s1.length();
int len2 = s2.length();
if (len1 > len2) { // a larger string should come first
return -1;
} else if (len1 == len2) {
// then compare by text:
return s1.compareTo(s2);
} else {
return 1;
}
}
}
private static final StringLengthComparator comparator = new StringLengthComparator();
private final IProject project;
private final IJavaProject jProject;
// if there is more than one output directory then the default output
// location to use is recorded in the 'defaultOutput' field
private File defaultOutput;
private Map<String, File> srcFolderToOutput = new TreeMap<String, File>(comparator);
private Map<File, IProject> binFolderToProject;
// maps files in the file system to IFolders in the workspace
// this keeps track of output locations
private final Map<String, IContainer> fileSystemPathToIContainer = new TreeMap<String, IContainer>(comparator);
private List<File> allOutputFolders = new ArrayList<File>();
// maps file system location to a path within the eclipse workspace
// needs to take into account linked sources, where the actual
// file system location may be different from the workspace location
private Map<String, String> allSourceFolders;
private boolean outputIsRoot;
// if there is only one output directory then this is recorded in the
// 'commonOutputDir' field.
private File commonOutputDir;
private IWorkspaceRoot workspaceRoot;
// Gather all of the files that are touched by this compilation
// and use it to determine which files need to have their
// Relationship maps updated.
private Set<File> compiledSourceFiles;
private FileURICache fileCache;
public CoreOutputLocationManager(IProject project, FileURICache fileCache) {
this.project = project;
this.fileCache = fileCache;
this.workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
jProject = JavaCore.create(project);
initSourceFolders();
if (!isUsingSeparateOutputFolders(jProject)) {
// using the same output directory therefore record this one
setCommonOutputDir();
allOutputFolders.add(commonOutputDir);
if (commonOutputDir != null) {
try {
if (outputIsRoot) {
fileSystemPathToIContainer.put(commonOutputDir.getAbsolutePath(),
project);
} else {
fileSystemPathToIContainer.put(commonOutputDir.getAbsolutePath(),
workspaceRoot.getFolder(jProject.getOutputLocation()));
}
} catch (JavaModelException e) {
}
}
} else {
// need to record all possible output directories
init();
}
}
public void buildStarting() {
compiledSourceFiles = new HashSet<File>();
}
public void buildComplete() {
compiledSourceFiles = null;
}
/**
* initialize the source folder locations only
*/
private void initSourceFolders() {
allSourceFolders = new TreeMap<String, String>(comparator);
try {
IClasspathEntry[] cpe = jProject.getRawClasspath();
for (int i = 0; i < cpe.length; i++) {
if (cpe[i].getEntryKind() == IClasspathEntry.CPE_SOURCE) {
IPath path = cpe[i].getPath();
IPath rawPath;
path = path.removeFirstSegments(1).makeRelative();
if (path.segmentCount() > 0) {
IFolder folder = project.getFolder(path);
rawPath = folder.getLocation();
} else {
rawPath = project.getLocation();
}
allSourceFolders.put(rawPath.toOSString(), path.toPortableString());
}
}
} catch (JavaModelException e) {
}
}
/**
* Calculate all the output locations
*/
private void init() {
outputIsRoot = false;
String inpathOutFolderString = getInpathOutputFolder();
boolean isUsingNonDefaultInpathOutfolder = inpathOutFolderString != null;
try {
IPath outputLocationPath = jProject.getOutputLocation();
defaultOutput = workspacePathToFile(outputLocationPath);
allOutputFolders.add(defaultOutput);
fileSystemPathToIContainer.put(defaultOutput.getAbsolutePath(),
project.getFullPath().equals(outputLocationPath)
? (IContainer) project
: (IContainer) workspaceRoot.getFolder(outputLocationPath));
IClasspathEntry[] cpe = jProject.getRawClasspath();
// store separate output folders in map
for (int i = 0; i < cpe.length; i++) {
if (cpe[i].getEntryKind() == IClasspathEntry.CPE_SOURCE) {
IPath output = cpe[i].getOutputLocation();
if (output != null) {
IPath path = cpe[i].getPath();
String srcFolder = path.removeFirstSegments(1).toPortableString();
if (path.segmentCount() == 1) { // output folder is project
srcFolder = path.toPortableString();
}
File out = workspacePathToFile(output);
srcFolderToOutput.put(srcFolder, out);
if (!allOutputFolders.contains(out)) {
allOutputFolders.add(out);
fileSystemPathToIContainer.put(out.getAbsolutePath(),
workspaceRoot.getFolder(output));
}
if (outputIsRoot) {
// bug 153682: if the project is the source folder
// then this output folder will always apply
defaultOutput = out;
}
}
}
}
// check to see if on inpath and need a special out folder for it.
if (isUsingNonDefaultInpathOutfolder) {
// first add the inpath out folder to the list of out folders
IPath inpathOutFolderPath = new Path(inpathOutFolderString);
IFolder inpathOutFolder = workspaceRoot.getFolder(inpathOutFolderPath);
File out = inpathOutFolder.getLocation().toFile();
fileSystemPathToIContainer.put(out.getAbsolutePath(),
workspaceRoot.getFolder(inpathOutFolderPath));
// now map everything coming from the in path to this out folder
for (int i = 0; i < cpe.length; i++) {
if (AspectJCorePreferences.isOnInpath(cpe[i])) {
// now must resolve the entry so that all jars contained in it
// are mapped.
List<IClasspathEntry> containerEntries = AspectJCorePreferences.resolveClasspath(cpe[i], project);
for (IClasspathEntry containerEntry : containerEntries) {
IPath path = containerEntry.getPath();
File f = workspacePathToFile(path);
if (f != null && f.exists()) {
// use full path
String srcFolder = new Path(f.getPath()).toPortableString();
srcFolderToOutput.put(srcFolder,out);
} else {
// outfolder does not exist
// probably because Project has been renamed
// and inpath output location has not been updated.
// this is handled with a message to the user
}
}
}
}
}
} catch (JavaModelException e) {
}
}
private boolean isComputingXmlFile() {
StackTraceElement[] stacktrace = Thread.currentThread().getStackTrace();
int max = stacktrace.length;
if (max > 6) max = 6;
for (int i=(max-1);i>0;i--) {
String s = stacktrace[i].toString();
// org.aspectj.ajdt.internal.core.builder.AjBuildManager.writeOutxmlFile(AjBuildManager.java:659) probably at i==4
if (s.startsWith("org.aspectj.ajdt.internal.core.builder.AjBuildManager.writeOutxmlFile")) {
return true;
}
}
return false;
}
public File getOutputLocationForClass(File compilationUnit) {
// remember that this file has been asked for
// presumably it is being recompiled
if (Util.isJavaLikeFileName(compilationUnit.getName()) && !isComputingXmlFile()) {
if (compiledSourceFiles == null) {
// pr540403
AJLog.log(AJLog.BUILDER,"Unexpectedly buildStarted() has not been called yet, unable to record: "+compilationUnit.getName());
} else {
compiledSourceFiles.add(compilationUnit);
}
}
return getOutputLocationForResource(compilationUnit);
}
public File getOutputLocationForResource(File resource) {
if (!isUsingSeparateOutputFolders(jProject)) {
return commonOutputDir;
}
if (resource == null || resource.toString() == null) {
return defaultOutput;
}
// due to linked files, there may be multiple IResource relating to a single File
IResource[] resources;
IFile[] files = fileCache.findFilesForURI(resource.toURI());
if (files != null && files.length > 0) {
resources = new IResource[files.length];
for (int i = 0; i < files.length; i++) {
resources[i] = files[i];
}
} else {
IContainer[] containers = fileCache.findContainersForURI(resource.toURI());
if (containers != null && containers.length > 0) {
resources = new IResource[containers.length];
for (int i = 0; i < containers.length; i++) {
resources[i] = containers[i];
}
} else {
resources = null;
}
}
String pathStr = null;
if (resources != null && resources.length > 0) {
// check if there is another version of the file
// that is linked to this project
// since the IResources are returned in an
// arbitrary order, doesn't matter which one we choose
IProject project = jProject.getProject();
for (int i = 0; i < resources.length; i++) {
if (resources[i].getProject().equals(project)) {
pathStr = resources[i].getFullPath().removeFirstSegments(1).toPortableString();
break;
}
}
if (pathStr == null) {
// this is from a location outside of the current project,
// use full path
pathStr = resources[0].getLocation().toPortableString();
}
}
if (pathStr == null) {
// if still null at this point, then assume that we have a fully qualified path
// coming from the inpath going to the inpath out folder
pathStr = new Path(resource.getPath()).toPortableString();
}
for (String src : srcFolderToOutput.keySet()) {
if (pathStr.startsWith(src)) {
File out = srcFolderToOutput.get(src);
return out;
}
}
// couldn't find anything
return defaultOutput;
}
/**
* @return true if there is more than one output directory being used by
* this project and false otherwise
*/
private boolean isUsingSeparateOutputFolders(IJavaProject jp) {
if (getInpathOutputFolder() != null) {
return true;
}
try {
IClasspathEntry[] cpe = jp.getRawClasspath();
for (int i = 0; i < cpe.length; i++) {
if (cpe[i].getEntryKind() == IClasspathEntry.CPE_SOURCE) {
if (cpe[i].getOutputLocation() != null) {
return true;
}
}
}
} catch (JavaModelException e) {
}
return false;
}
/**
* Record the 'common output directory', namely the one where all the output
* goes
*/
private void setCommonOutputDir() {
IJavaProject jProject = JavaCore.create(project);
IPath workspaceRelativeOutputPath;
try {
workspaceRelativeOutputPath = jProject.getOutputLocation();
} catch (JavaModelException e) {
commonOutputDir = project.getLocation().toFile();
outputIsRoot = true;
return;
}
if (workspaceRelativeOutputPath.segmentCount() == 1) {
commonOutputDir = jProject.getResource().getLocation().toFile();
outputIsRoot = true;
return;
}
IFolder out = ResourcesPlugin.getWorkspace().getRoot().getFolder(workspaceRelativeOutputPath);
commonOutputDir = out.getLocation().toFile();
}
private File workspacePathToFile(IPath path) {
if (path.segmentCount() == 1) {
// bug 153682: getFolder fails when the path is a project
IResource res = workspaceRoot.findMember(path);
outputIsRoot = true;
return res.getLocation().toFile();
}
IPath outPath;
IResource out;
String fileExtension = path.getFileExtension();
if (fileExtension != null) {
// assume a file
out = workspaceRoot.getFile(path);
} else {
out = workspaceRoot.getFolder(path);
}
if (out.getProject().exists()) {
outPath = out.getLocation();
} else {
// maybe this is a fully qualified path
outPath = path;
}
if (outPath != null) {
return outPath.toFile();
} else {
return null;
}
}
/**
* return all output directories used by this project
*/
public List<File> getAllOutputLocations() {
return allOutputFolders;
}
public String getInpathOutputFolder() {
String inpathOutFolder = AspectJCorePreferences.getProjectInpathOutFolder(project);
// assume that the folder is valid...
// null means that the default out folder is used
return inpathOutFolder;
}
/**
* Called when build is completed. Respond with all source files compiled
* for this build
* @return all source files compiled for this build
*/
public File[] getCompiledSourceFiles() {
return compiledSourceFiles == null ? new File[0] : compiledSourceFiles.toArray(new File[compiledSourceFiles.size()]);
}
/**
* If there's only one output directory return this one, otherwise return
* the one marked as default
*/
public File getDefaultOutputLocation() {
if (!isUsingSeparateOutputFolders(jProject)) {
return commonOutputDir;
} else {
return defaultOutput;
}
}
public String getSourceFolderForFile(File sourceFile) {
String sourceFilePath = sourceFile.getAbsolutePath();
for (Entry<String, String> sourceFolderMapping : allSourceFolders.entrySet()) {
if (sourceFilePath.startsWith(sourceFolderMapping.getKey())) {
return sourceFolderMapping.getValue();
}
}
// might be a linked folder in a source folder
IFile[] files = fileCache.findFilesForURI(sourceFile.toURI());
try {
IPackageFragmentRoot[] roots = jProject.getPackageFragmentRoots();
for (IPackageFragmentRoot root : roots) {
if (!root.isReadOnly()) {
IContainer container = (IContainer) root.getResource();
for (IFile file : files) {
if (container.getFullPath().isPrefixOf(file.getFullPath())) {
return allSourceFolders.get(container.getLocation().toOSString());
}
}
}
}
} catch (JavaModelException e) {
}
return null;
}
public void reportFileRemove(String outFileStr, int fileType) {
for (Entry<String, IContainer> entry : fileSystemPathToIContainer.entrySet()) {
String outFolderStr = entry.getKey();
if (outFileStr.startsWith(outFolderStr)) {
IContainer outFolder = entry.getValue();
IFile outFile = outFolder.getFile(new Path(outFileStr.substring(outFolderStr.length())));
try {
outFile.refreshLocal(IResource.DEPTH_ZERO, null);
return;
} catch (CoreException e) {
}
}
}
}
public Map<File,String> getInpathMap() {
return Collections.emptyMap();
}
/**
* This method must do two things. First, it performs a refreshLocal on
* the newly changed file. Then it marks the file and all of its parent
* folders as derived (up to, but not including the output folder itself).
* Folders are marked as derived only if the output folder is not the same
* as the source folder.
*/
public void reportFileWrite(String outFileStr, int fileType) {
try {
outer:
for (Entry<String, IContainer> entry : fileSystemPathToIContainer.entrySet()) {
String outFolderStr = entry.getKey();
if (outFileStr.startsWith(outFolderStr)) {
IContainer outFolder = entry.getValue();
IFile outFile = outFolder.getFile(new Path(outFileStr.substring(outFolderStr.length())));
outFile.refreshLocal(IResource.DEPTH_ZERO, null);
if (outFile.exists()) {
// if this is a resource whose source folder and out folder are the same,
// do not mark as derived
boolean outputIsSourceFolder = isOutFolderASourceFolder(outFolder);
if (! isResourceInSourceFolder(outFile, outputIsSourceFolder)) {
outFile.setDerived(true, null);
}
// only do this if output is not a source folder
if (!outputIsSourceFolder) {
IContainer parent = outFile.getParent();
inner:
while (!parent.equals(outFolder) ) {
if (!parent.isDerived()) {
parent.setDerived(true, null);
} else {
// no need to continnue
// assume that all folders are derived all the way up
break inner;
}
parent = parent.getParent();
if (parent == null) {
// shouldn't happen
break inner;
}
}
}
break outer;
}
}
}
} catch (CoreException e) {
}
}
private boolean isResourceInSourceFolder(IFile outFile,
boolean outputIsSourceFolder) {
return !(outFile.getFileExtension() != null && outFile.getFileExtension().equals("class"))
&& outputIsSourceFolder;
}
private boolean isOutFolderASourceFolder(IContainer outFolder) {
return outputIsRoot || srcFolderToOutput.containsKey(outFolder.getFullPath().removeFirstSegments(1).makeRelative().toPortableString());
}
/**
* Return the Java project that has outputFolder as an output location, or null if it is
* not recognized.
*
* This method can return null if outputFolder is not found
* in any declaring project
*
*/
protected IProject findDeclaringProject(File outputFolder) {
if (binFolderToProject == null) {
initDeclaringProjectsMap();
}
return binFolderToProject.get(outputFolder);
}
/**
* the field binFolderToProject must be refreshed before each build
* because we are not sure if any bin folders in downstream projects
* have changed.
*
* See bug 270335
*/
protected void zapBinFolderToProjectMap() {
binFolderToProject = null;
}
/**
* Initialize the binFolderToProject map so that the map contains
* java.io.File -> IProject where the file is an output location
* and the project is where this output location is defined
*/
private void initDeclaringProjectsMap() {
AJLog.logStart("OutputLocationManager: binary folder to declaring project map creation: " + project);
binFolderToProject = new HashMap<File, IProject>();
IJavaProject jp = jProject;
try {
mapProject(jp);
} catch (JavaModelException e) {
}
AJLog.logEnd(AJLog.BUILDER_CLASSPATH, "OutputLocationManager: binary folder to declaring project map creation: " + project);
}
private void mapProject(IJavaProject jp) throws JavaModelException {
IClasspathEntry[] cpes = jp.getRawClasspath();
for (int i = 0; i < cpes.length; i++) {
if (cpes[i].isExported() ||
cpes[i].getEntryKind() == IClasspathEntry.CPE_SOURCE ||
jp == jProject) {
handleClassPathEntry(jp, cpes[i]);
}
}
}
private void handleClassPathEntry(IJavaProject jp, IClasspathEntry cpe) throws JavaModelException {
switch (cpe.getEntryKind()) {
case IClasspathEntry.CPE_CONTAINER:
IClasspathContainer container =
JavaCore.getClasspathContainer(cpe.getPath(), jp);
if (container != null && container.getKind() != IClasspathContainer.K_DEFAULT_SYSTEM) {
IClasspathEntry[] cpes = container.getClasspathEntries();
for (int i = 0; i < cpes.length; i++) {
handleClassPathEntry(jp, cpes[i]);
}
}
break;
case IClasspathEntry.CPE_LIBRARY:
File libFile = pathToFile(cpe.getPath());
if (libFile.isDirectory()) { // ignore jar files
if (libFile != null && !binFolderToProject.containsKey(libFile)) {
binFolderToProject.put(libFile, jp.getProject());
}
}
break;
case IClasspathEntry.CPE_PROJECT:
IJavaProject jpClasspath = pathToProject(cpe.getPath());
if (jpClasspath != null) {
mapProject(jpClasspath);
}
break;
case IClasspathEntry.CPE_SOURCE:
File outFile = pathToFile(cpe.getOutputLocation() == null ? jp.getOutputLocation() : cpe.getOutputLocation());
if (outFile != null && ! binFolderToProject.containsKey(outFile)) {
binFolderToProject.put(outFile, jp.getProject());
}
break;
case IClasspathEntry.CPE_VARIABLE:
IClasspathEntry cpeResolved = JavaCore.getResolvedClasspathEntry(cpe);
if (cpeResolved != null) {
handleClassPathEntry(jp, cpeResolved);
}
break;
}
}
private IJavaProject pathToProject(IPath path) {
if (path != null && path.segmentCount() > 0) {
IProject p = ResourcesPlugin.getWorkspace().getRoot().getProject(path.segments()[0]);
return JavaCore.create(p);
} else {
return null;
}
}
private File pathToFile(IPath path) {
// Bug 279497 if path is only one segment, then assume it is a project
IPath locPath = path.segmentCount() > 1 ?
ResourcesPlugin.getWorkspace().getRoot().getFolder(path).getLocation() :
ResourcesPlugin.getWorkspace().getRoot().getProject(path.makeRelative().toOSString()).getLocation();
File f;
if (locPath != null) {
f = locPath.toFile();
} else {
f = path.toFile();
}
return f;
}
/**
* Aim of this callback from the compiler is to ask Eclipse if it knows which project has the
* supplied directory as an output folder, and if that can be determined then look at the
* last structural build time of that project and any structurally changed types since that
* build time. If it doesn't look like anything has changed since the supplied buildtime then
* we assume that means nothing changed in the directory and so do not need to check the time
* stamp of each file within it.
*
* This method does nothing more than a rudimentary analysis - if there are changes then it does
* not currently attempt to determine if they are interesting (ie. whether they are changes to
* types that the compiler asking the question depends upon).
*/
public int discoverChangesSince(File dir, long buildtime) {
IProject project = findDeclaringProject(dir);
try {
if (project!=null) {
Object s = JavaModelManager.getJavaModelManager().getLastBuiltState(project, null);
if (s != null && s instanceof State) {
State state = (State) s;
long dependeeTime = getLastStructuralBuildTime(state);
if (dependeeTime < buildtime) {
StringSet changes = getStructurallyChangedTypes(state);
// this test isn't quite right... but it basically works
if (changes==null || changes.elementSize==0) {
return 1; // no changes at all (doesnt determine whether they are of interest)
}
}
}
}
} catch (Exception e) {
}
return 0; // DONTKNOW - this will cause the caller to do the .class modtime tests
}
private static long getLastStructuralBuildTime(State state)
throws Exception {
if (lastStructuralBuildTimeField == null) {
lastStructuralBuildTimeField = State.class.getDeclaredField("lastStructuralBuildTime");
lastStructuralBuildTimeField.setAccessible(true);
}
return lastStructuralBuildTimeField.getLong(state);
}
private static StringSet getStructurallyChangedTypes(State state)
throws Exception {
if (structurallyChangedTypesField == null) {
structurallyChangedTypesField = State.class.getDeclaredField("structurallyChangedTypes");
structurallyChangedTypesField.setAccessible(true);
}
return (StringSet)structurallyChangedTypesField.get(state);
}
// Cached for performance reasons
private static java.lang.reflect.Field lastStructuralBuildTimeField = null;
private static java.lang.reflect.Field structurallyChangedTypesField = null;
}