/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
*
* 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 com.android.ide.eclipse.adt.internal.sdk;
import com.android.ddmlib.IDevice;
import com.android.ide.common.rendering.LayoutLibrary;
import com.android.ide.common.sdk.LoadStatus;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.AndroidConstants;
import com.android.ide.eclipse.adt.internal.build.DexWrapper;
import com.android.ide.eclipse.adt.internal.project.AndroidClasspathContainerInitializer;
import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor;
import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener;
import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IProjectListener;
import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IResourceEventListener;
import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryDifference;
import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryState;
import com.android.io.StreamException;
import com.android.prefs.AndroidLocation.AndroidLocationException;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.ISdkLog;
import com.android.sdklib.SdkConstants;
import com.android.sdklib.SdkManager;
import com.android.sdklib.internal.avd.AvdManager;
import com.android.sdklib.internal.project.ProjectProperties;
import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy;
import com.android.sdklib.internal.project.ProjectProperties.PropertyType;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IMarkerDelta;
import org.eclipse.core.resources.IPathVariableManager;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
/**
* Central point to load, manipulate and deal with the Android SDK. Only one SDK can be used
* at the same time.
*
* To start using an SDK, call {@link #loadSdk(String)} which returns the instance of
* the Sdk object.
*
* To get the list of platforms or add-ons present in the SDK, call {@link #getTargets()}.
*/
public final class Sdk {
private static final String PROP_LIBRARY = "_library"; //$NON-NLS-1$
private static final String PROP_LIBRARY_NAME = "_library_name"; //$NON-NLS-1$
public static final String CREATOR_ADT = "ADT"; //$NON-NLS-1$
public static final String PROP_CREATOR = "_creator"; //$NON-NLS-1$
private final static Object sLock = new Object();
private static Sdk sCurrentSdk = null;
/**
* Map associating {@link IProject} and their state {@link ProjectState}.
* <p/>This <b>MUST NOT</b> be accessed directly. Instead use {@link #getProjectState(IProject)}.
*/
private final static HashMap<IProject, ProjectState> sProjectStateMap =
new HashMap<IProject, ProjectState>();
/**
* Data bundled using during the load of Target data.
* <p/>This contains the {@link LoadStatus} and a list of projects that attempted
* to compile before the loading was finished. Those projects will be recompiled
* at the end of the loading.
*/
private final static class TargetLoadBundle {
LoadStatus status;
final HashSet<IJavaProject> projecsToReload = new HashSet<IJavaProject>();
}
private final SdkManager mManager;
private final DexWrapper mDexWrapper;
private final AvdManager mAvdManager;
/** Map associating an {@link IAndroidTarget} to an {@link AndroidTargetData} */
private final HashMap<IAndroidTarget, AndroidTargetData> mTargetDataMap =
new HashMap<IAndroidTarget, AndroidTargetData>();
/** Map associating an {@link IAndroidTarget} and its {@link TargetLoadBundle}. */
private final HashMap<IAndroidTarget, TargetLoadBundle> mTargetDataStatusMap =
new HashMap<IAndroidTarget, TargetLoadBundle>();
private final String mDocBaseUrl;
private final LayoutDeviceManager mLayoutDeviceManager = new LayoutDeviceManager();
/**
* Classes implementing this interface will receive notification when targets are changed.
*/
public interface ITargetChangeListener {
/**
* Sent when project has its target changed.
*/
void onProjectTargetChange(IProject changedProject);
/**
* Called when the targets are loaded (either the SDK finished loading when Eclipse starts,
* or the SDK is changed).
*/
void onTargetLoaded(IAndroidTarget target);
/**
* Called when the base content of the SDK is parsed.
*/
void onSdkLoaded();
}
/**
* Basic abstract implementation of the ITargetChangeListener for the case where both
* {@link #onProjectTargetChange(IProject)} and {@link #onTargetLoaded(IAndroidTarget)}
* use the same code based on a simple test requiring to know the current IProject.
*/
public static abstract class TargetChangeListener implements ITargetChangeListener {
/**
* Returns the {@link IProject} associated with the listener.
*/
public abstract IProject getProject();
/**
* Called when the listener needs to take action on the event. This is only called
* if {@link #getProject()} and the {@link IAndroidTarget} associated with the project
* match the values received in {@link #onProjectTargetChange(IProject)} and
* {@link #onTargetLoaded(IAndroidTarget)}.
*/
public abstract void reload();
public void onProjectTargetChange(IProject changedProject) {
if (changedProject != null && changedProject.equals(getProject())) {
reload();
}
}
public void onTargetLoaded(IAndroidTarget target) {
IProject project = getProject();
if (target != null && target.equals(Sdk.getCurrent().getTarget(project))) {
reload();
}
}
public void onSdkLoaded() {
// do nothing;
}
}
/**
* Returns the lock object used to synchronize all operations dealing with SDK, targets and
* projects.
*/
public static final Object getLock() {
return sLock;
}
/**
* Loads an SDK and returns an {@link Sdk} object if success.
* <p/>If the SDK failed to load, it displays an error to the user.
* @param sdkLocation the OS path to the SDK.
*/
public static Sdk loadSdk(String sdkLocation) {
synchronized (sLock) {
if (sCurrentSdk != null) {
sCurrentSdk.dispose();
sCurrentSdk = null;
}
final ArrayList<String> logMessages = new ArrayList<String>();
ISdkLog log = new ISdkLog() {
public void error(Throwable throwable, String errorFormat, Object... arg) {
if (errorFormat != null) {
logMessages.add(String.format("Error: " + errorFormat, arg));
}
if (throwable != null) {
logMessages.add(throwable.getMessage());
}
}
public void warning(String warningFormat, Object... arg) {
logMessages.add(String.format("Warning: " + warningFormat, arg));
}
public void printf(String msgFormat, Object... arg) {
logMessages.add(String.format(msgFormat, arg));
}
};
// get an SdkManager object for the location
SdkManager manager = SdkManager.createManager(sdkLocation, log);
if (manager != null) {
// load DX.
DexWrapper dexWrapper = new DexWrapper();
String dexLocation =
sdkLocation + File.separator +
SdkConstants.OS_SDK_PLATFORM_TOOLS_LIB_FOLDER + SdkConstants.FN_DX_JAR;
IStatus res = dexWrapper.loadDex(dexLocation);
if (res != Status.OK_STATUS) {
log.error(null, res.getMessage());
dexWrapper = null;
}
// create the AVD Manager
AvdManager avdManager = null;
try {
avdManager = new AvdManager(manager, log);
} catch (AndroidLocationException e) {
log.error(e, "Error parsing the AVDs");
}
sCurrentSdk = new Sdk(manager, dexWrapper, avdManager);
return sCurrentSdk;
} else {
StringBuilder sb = new StringBuilder("Error Loading the SDK:\n");
for (String msg : logMessages) {
sb.append('\n');
sb.append(msg);
}
AdtPlugin.displayError("Android SDK", sb.toString());
}
return null;
}
}
/**
* Returns the current {@link Sdk} object.
*/
public static Sdk getCurrent() {
synchronized (sLock) {
return sCurrentSdk;
}
}
/**
* Returns the location (OS path) of the current SDK.
*/
public String getSdkLocation() {
return mManager.getLocation();
}
/**
* Returns the URL to the local documentation.
* Can return null if no documentation is found in the current SDK.
*
* @return A file:// URL on the local documentation folder if it exists or null.
*/
public String getDocumentationBaseUrl() {
return mDocBaseUrl;
}
/**
* Returns the list of targets that are available in the SDK.
*/
public IAndroidTarget[] getTargets() {
return mManager.getTargets();
}
/**
* Returns a target from a hash that was generated by {@link IAndroidTarget#hashString()}.
*
* @param hash the {@link IAndroidTarget} hash string.
* @return The matching {@link IAndroidTarget} or null.
*/
public IAndroidTarget getTargetFromHashString(String hash) {
return mManager.getTargetFromHashString(hash);
}
/**
* Initializes a new project with a target. This creates the <code>default.properties</code>
* file.
* @param project the project to intialize
* @param target the project's target.
* @throws IOException if creating the file failed in any way.
* @throws StreamException
*/
public void initProject(IProject project, IAndroidTarget target)
throws IOException, StreamException {
if (project == null || target == null) {
return;
}
synchronized (sLock) {
// check if there's already a state?
ProjectState state = getProjectState(project);
ProjectPropertiesWorkingCopy properties = null;
if (state != null) {
properties = state.getProperties().makeWorkingCopy();
}
if (properties == null) {
IPath location = project.getLocation();
if (location == null) { // can return null when the project is being deleted.
// do nothing and return null;
return;
}
properties = ProjectProperties.create(location.toOSString(), PropertyType.DEFAULT);
}
// save the target hash string in the project persistent property
properties.setProperty(ProjectProperties.PROPERTY_TARGET, target.hashString());
properties.save();
}
}
/**
* Returns the {@link ProjectState} object associated with a given project.
* <p/>
* This method is the only way to properly get the project's {@link ProjectState}
* If the project has not yet been loaded, then it is loaded.
* <p/>Because this methods deals with projects, it's not linked to an actual {@link Sdk}
* objects, and therefore is static.
* <p/>The value returned by {@link ProjectState#getTarget()} will change as {@link Sdk} objects
* are replaced.
* @param project the request project
* @return the ProjectState for the project.
*/
public static ProjectState getProjectState(IProject project) {
if (project == null) {
return null;
}
synchronized (sLock) {
ProjectState state = sProjectStateMap.get(project);
if (state == null) {
// load the default.properties from the project folder.
IPath location = project.getLocation();
if (location == null) { // can return null when the project is being deleted.
// do nothing and return null;
return null;
}
ProjectProperties properties = ProjectProperties.load(location.toOSString(),
PropertyType.DEFAULT);
if (properties == null) {
AdtPlugin.log(IStatus.ERROR, "Failed to load properties file for project '%s'",
project.getName());
return null;
}
state = new ProjectState(project, properties);
sProjectStateMap.put(project, state);
// try to resolve the target
if (AdtPlugin.getDefault().getSdkLoadStatus() == LoadStatus.LOADED) {
sCurrentSdk.loadTarget(state);
}
}
return state;
}
}
/**
* Returns the {@link IAndroidTarget} object associated with the given {@link IProject}.
*/
public IAndroidTarget getTarget(IProject project) {
if (project == null) {
return null;
}
ProjectState state = getProjectState(project);
if (state != null) {
return state.getTarget();
}
return null;
}
/**
* Loads the {@link IAndroidTarget} for a given project.
* <p/>This method will get the target hash string from the project properties, and resolve
* it to an {@link IAndroidTarget} object and store it inside the {@link ProjectState}.
* @param state the state representing the project to load.
* @return the target that was loaded.
*/
public IAndroidTarget loadTarget(ProjectState state) {
IAndroidTarget target = null;
String hash = state.getTargetHashString();
if (hash != null) {
state.setTarget(target = getTargetFromHashString(hash));
}
return target;
}
/**
* Checks and loads (if needed) the data for a given target.
* <p/> The data is loaded in a separate {@link Job}, and opened editors will be notified
* through their implementation of {@link ITargetChangeListener#onTargetLoaded(IAndroidTarget)}.
* <p/>An optional project as second parameter can be given to be recompiled once the target
* data is finished loading.
* <p/>The return value is non-null only if the target data has already been loaded (and in this
* case is the status of the load operation)
* @param target the target to load.
* @param project an optional project to be recompiled when the target data is loaded.
* If the target is already loaded, nothing happens.
* @return The load status if the target data is already loaded.
*/
public LoadStatus checkAndLoadTargetData(final IAndroidTarget target, IJavaProject project) {
boolean loadData = false;
synchronized (sLock) {
TargetLoadBundle bundle = mTargetDataStatusMap.get(target);
if (bundle == null) {
bundle = new TargetLoadBundle();
mTargetDataStatusMap.put(target,bundle);
// set status to loading
bundle.status = LoadStatus.LOADING;
// add project to bundle
if (project != null) {
bundle.projecsToReload.add(project);
}
// and set the flag to start the loading below
loadData = true;
} else if (bundle.status == LoadStatus.LOADING) {
// add project to bundle
if (project != null) {
bundle.projecsToReload.add(project);
}
return bundle.status;
} else if (bundle.status == LoadStatus.LOADED || bundle.status == LoadStatus.FAILED) {
return bundle.status;
}
}
if (loadData) {
Job job = new Job(String.format("Loading data for %1$s", target.getFullName())) {
@Override
protected IStatus run(IProgressMonitor monitor) {
AdtPlugin plugin = AdtPlugin.getDefault();
try {
IStatus status = new AndroidTargetParser(target).run(monitor);
IJavaProject[] javaProjectArray = null;
synchronized (sLock) {
TargetLoadBundle bundle = mTargetDataStatusMap.get(target);
if (status.getCode() != IStatus.OK) {
bundle.status = LoadStatus.FAILED;
bundle.projecsToReload.clear();
} else {
bundle.status = LoadStatus.LOADED;
// Prepare the array of project to recompile.
// The call is done outside of the synchronized block.
javaProjectArray = bundle.projecsToReload.toArray(
new IJavaProject[bundle.projecsToReload.size()]);
// and update the UI of the editors that depend on the target data.
plugin.updateTargetListeners(target);
}
}
if (javaProjectArray != null) {
AndroidClasspathContainerInitializer.updateProjects(javaProjectArray);
}
return status;
} catch (Throwable t) {
synchronized (sLock) {
TargetLoadBundle bundle = mTargetDataStatusMap.get(target);
bundle.status = LoadStatus.FAILED;
}
AdtPlugin.log(t, "Exception in checkAndLoadTargetData."); //$NON-NLS-1$
return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
String.format(
"Parsing Data for %1$s failed", //$NON-NLS-1$
target.hashString()),
t);
}
}
};
job.setPriority(Job.BUILD); // build jobs are run after other interactive jobs
job.schedule();
}
// The only way to go through here is when the loading starts through the Job.
// Therefore the current status of the target is LOADING.
return LoadStatus.LOADING;
}
/**
* Return the {@link AndroidTargetData} for a given {@link IAndroidTarget}.
*/
public AndroidTargetData getTargetData(IAndroidTarget target) {
synchronized (sLock) {
return mTargetDataMap.get(target);
}
}
/**
* Return the {@link AndroidTargetData} for a given {@link IProject}.
*/
public AndroidTargetData getTargetData(IProject project) {
synchronized (sLock) {
IAndroidTarget target = getTarget(project);
if (target != null) {
return getTargetData(target);
}
}
return null;
}
/**
* Returns a {@link DexWrapper} object to be used to execute dx commands. If dx.jar was not
* loaded properly, then this will return <code>null</code>.
*/
public DexWrapper getDexWrapper() {
return mDexWrapper;
}
/**
* Returns the {@link AvdManager}. If the AvdManager failed to parse the AVD folder, this could
* be <code>null</code>.
*/
public AvdManager getAvdManager() {
return mAvdManager;
}
public static AndroidVersion getDeviceVersion(IDevice device) {
try {
Map<String, String> props = device.getProperties();
String apiLevel = props.get(IDevice.PROP_BUILD_API_LEVEL);
if (apiLevel == null) {
return null;
}
return new AndroidVersion(Integer.parseInt(apiLevel),
props.get((IDevice.PROP_BUILD_CODENAME)));
} catch (NumberFormatException e) {
return null;
}
}
public LayoutDeviceManager getLayoutDeviceManager() {
return mLayoutDeviceManager;
}
/**
* Returns a list of {@link ProjectState} representing projects depending, directly or
* indirectly on a given library project.
* @param project the library project.
* @return a possibly empty list of ProjectState.
*/
public static Set<ProjectState> getMainProjectsFor(IProject project) {
synchronized (sLock) {
// first get the project directly depending on this.
HashSet<ProjectState> list = new HashSet<ProjectState>();
// loop on all project and see if ProjectState.getLibrary returns a non null
// project.
for (Entry<IProject, ProjectState> entry : sProjectStateMap.entrySet()) {
if (project != entry.getKey()) {
LibraryState library = entry.getValue().getLibrary(project);
if (library != null) {
list.add(entry.getValue());
}
}
}
// now look for projects depending on the projects directly depending on the library.
HashSet<ProjectState> result = new HashSet<ProjectState>(list);
for (ProjectState p : list) {
if (p.isLibrary()) {
Set<ProjectState> set = getMainProjectsFor(p.getProject());
result.addAll(set);
}
}
return result;
}
}
private Sdk(SdkManager manager, DexWrapper dexWrapper, AvdManager avdManager) {
mManager = manager;
mDexWrapper = dexWrapper;
mAvdManager = avdManager;
// listen to projects closing
GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor();
monitor.addProjectListener(mProjectListener);
monitor.addFileListener(mFileListener, IResourceDelta.CHANGED | IResourceDelta.ADDED);
monitor.addResourceEventListener(mResourceEventListener);
// pre-compute some paths
mDocBaseUrl = getDocumentationBaseUrl(mManager.getLocation() +
SdkConstants.OS_SDK_DOCS_FOLDER);
// load the built-in and user layout devices
mLayoutDeviceManager.loadDefaultAndUserDevices(mManager.getLocation());
// and the ones from the add-on
loadLayoutDevices();
// update whatever ProjectState is already present with new IAndroidTarget objects.
synchronized (sLock) {
for (Entry<IProject, ProjectState> entry: sProjectStateMap.entrySet()) {
entry.getValue().setTarget(
getTargetFromHashString(entry.getValue().getTargetHashString()));
}
}
}
/**
* Cleans and unloads the SDK.
*/
private void dispose() {
GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor();
monitor.removeProjectListener(mProjectListener);
monitor.removeFileListener(mFileListener);
monitor.removeResourceEventListener(mResourceEventListener);
// the IAndroidTarget objects are now obsolete so update the project states.
synchronized (sLock) {
for (Entry<IProject, ProjectState> entry: sProjectStateMap.entrySet()) {
entry.getValue().setTarget(null);
}
}
}
void setTargetData(IAndroidTarget target, AndroidTargetData data) {
synchronized (sLock) {
mTargetDataMap.put(target, data);
}
}
/**
* Returns the URL to the local documentation.
* Can return null if no documentation is found in the current SDK.
*
* @param osDocsPath Path to the documentation folder in the current SDK.
* The folder may not actually exist.
* @return A file:// URL on the local documentation folder if it exists or null.
*/
private String getDocumentationBaseUrl(String osDocsPath) {
File f = new File(osDocsPath);
if (f.isDirectory()) {
try {
// Note: to create a file:// URL, one would typically use something like
// f.toURI().toURL().toString(). However this generates a broken path on
// Windows, namely "C:\\foo" is converted to "file:/C:/foo" instead of
// "file:///C:/foo" (i.e. there should be 3 / after "file:"). So we'll
// do the correct thing manually.
String path = f.getAbsolutePath();
if (File.separatorChar != '/') {
path = path.replace(File.separatorChar, '/');
}
// For some reason the URL class doesn't add the mandatory "//" after
// the "file:" protocol name, so it has to be hacked into the path.
URL url = new URL("file", null, "//" + path); //$NON-NLS-1$ //$NON-NLS-2$
String result = url.toString();
return result;
} catch (MalformedURLException e) {
// ignore malformed URLs
}
}
return null;
}
/**
* Parses the SDK add-ons to look for files called {@link SdkConstants#FN_DEVICES_XML} to
* load {@link LayoutDevice} from them.
*/
private void loadLayoutDevices() {
IAndroidTarget[] targets = mManager.getTargets();
for (IAndroidTarget target : targets) {
if (target.isPlatform() == false) {
File deviceXml = new File(target.getLocation(), SdkConstants.FN_DEVICES_XML);
if (deviceXml.isFile()) {
mLayoutDeviceManager.parseAddOnLayoutDevice(deviceXml);
}
}
}
mLayoutDeviceManager.sealAddonLayoutDevices();
}
/**
* Delegate listener for project changes.
*/
private IProjectListener mProjectListener = new IProjectListener() {
public void projectClosed(IProject project) {
onProjectRemoved(project, false /*deleted*/);
}
public void projectDeleted(IProject project) {
onProjectRemoved(project, true /*deleted*/);
}
private void onProjectRemoved(IProject project, boolean deleted) {
try {
if (project.hasNature(AndroidConstants.NATURE_DEFAULT) == false) {
return;
}
} catch (CoreException e) {
// this can only happen if the project does not exist or is not open, neither
// of which can happen here since we're processing a Project removed/deleted event
// which is processed before the project is actually removed/closed.
}
// get the target project
synchronized (sLock) {
// Don't use getProject() as it could create the ProjectState if it's not
// there yet and this is not what we want. We want the current object.
// Therefore, direct access to the map.
ProjectState state = sProjectStateMap.get(project);
if (state != null) {
// 1. clear the layout lib cache associated with this project
IAndroidTarget target = state.getTarget();
if (target != null) {
// get the bridge for the target, and clear the cache for this project.
AndroidTargetData data = mTargetDataMap.get(target);
if (data != null) {
LayoutLibrary layoutLib = data.getLayoutLibrary();
if (layoutLib != null && layoutLib.getStatus() == LoadStatus.LOADED) {
layoutLib.clearCaches(project);
}
}
}
// 2. if the project is a library, make sure to update the
// LibraryState for any main project using this.
// Also, record the updated projects that are libraries, to update
// projects that depend on them.
ArrayList<ProjectState> updatedLibraries = new ArrayList<ProjectState>();
for (ProjectState projectState : sProjectStateMap.values()) {
LibraryState libState = projectState.getLibrary(project);
if (libState != null) {
// get the current libraries.
List<IProject> oldLibraries = projectState.getFullLibraryProjects();
// the unlink below will work in the job, but we need to close
// the library right away.
// This is because in case of a rename of a project, projectClosed and
// projectOpened will be called before any other job is run, so we
// need to make sure projectOpened is closed with the main project
// state up to date.
libState.close();
// edit the project to remove the linked source folder.
// this also calls LibraryState.close();
LinkUpdateBundle bundle = getLinkBundle(projectState, oldLibraries);
if (bundle != null) {
queueLinkUpdateBundle(bundle);
}
if (projectState.isLibrary()) {
updatedLibraries.add(projectState);
}
}
}
if (deleted) {
// remove the linked path variable
disposeLibraryProject(project);
}
// now remove the project for the project map.
sProjectStateMap.remove(project);
// update the projects that depend on the updated project
updateProjectsWithNewLibraries(updatedLibraries);
}
}
}
public void projectOpened(IProject project) {
onProjectOpened(project);
}
public void projectOpenedWithWorkspace(IProject project) {
// no need to force recompilation when projects are opened with the workspace.
onProjectOpened(project);
}
private void onProjectOpened(final IProject openedProject) {
try {
if (openedProject.hasNature(AndroidConstants.NATURE_DEFAULT) == false) {
return;
}
} catch (CoreException e) {
// this can only happen if the project does not exist or is not open, neither
// of which can happen here since we're processing a Project opened event.
}
ProjectState openedState = getProjectState(openedProject);
if (openedState != null) {
if (openedState.hasLibraries()) {
// list of library to link to the opened project.
final ArrayList<IProject> libsToLink = new ArrayList<IProject>();
// Look for all other opened projects to see if any is a library for the opened
// project.
synchronized (sLock) {
for (ProjectState projectState : sProjectStateMap.values()) {
if (projectState != openedState) {
// ProjectState#needs() both checks if this is a missing library
// and updates LibraryState to contains the new values.
LibraryState libState = openedState.needs(projectState);
if (libState != null) {
// we have a match! Add the library to the list (if it was
// not added through an indirect dependency before).
IProject libProject = libState.getProjectState().getProject();
if (libsToLink.contains(libProject) == false) {
libsToLink.add(libProject);
}
// now find what this depends on, and add it too.
// The order here doesn't matter
// as it's just to add the linked source folder, so there's no
// need to use ProjectState#getFullLibraryProjects() which
// could return project that have already been added anyway.
fillProjectDependenciesList(libState.getProjectState(),
libsToLink);
}
}
}
}
// create a link bundle always, because even if there's no libraries to add
// to the CPE, the cleaning of invalid CPE must happen.
LinkUpdateBundle bundle = new LinkUpdateBundle();
bundle.mProject = openedProject;
bundle.mNewLibraryProjects = libsToLink.toArray(
new IProject[libsToLink.size()]);
bundle.mCleanupCPE = true;
queueLinkUpdateBundle(bundle);
}
// if the project is a library, then add it to the list of projects being opened.
// They will be processed in IResourceEventListener#resourceChangeEventEnd.
// This is done so that we are sure to process all the projects being opened
// first and only then process projects depending on the projects that were opened.
if (openedState.isLibrary()) {
setupLibraryProject(openedProject);
mOpenedLibraryProjects.add(openedState);
}
}
}
public void projectRenamed(IProject project, IPath from) {
try {
if (project.hasNature(AndroidConstants.NATURE_DEFAULT) == false) {
return;
}
} catch (CoreException e) {
// this can only happen if the project does not exist or is not open, neither
// of which can happen here since we're processing a Project renamed event.
}
// a project was renamed.
// if the project is a library, look for any project that depended on it
// and update it. (default.properties and linked source folder)
ProjectState renamedState = getProjectState(project);
if (renamedState.isLibrary()) {
// remove the variable
disposeLibraryProject(from.lastSegment());
// update the project depending on the library
synchronized (sLock) {
for (ProjectState projectState : sProjectStateMap.values()) {
if (projectState != renamedState && projectState.isMissingLibraries()) {
IPath oldRelativePath = from.makeRelativeTo(
projectState.getProject().getFullPath());
IPath newRelativePath = project.getFullPath().makeRelativeTo(
projectState.getProject().getFullPath());
// get the current libraries
List<IProject> oldLibraries = projectState.getFullLibraryProjects();
// update the library for the main project.
LibraryState libState = projectState.updateLibrary(
oldRelativePath.toString(), newRelativePath.toString(),
renamedState);
if (libState != null) {
// this project depended on the renamed library, create a bundle
// with the whole library difference (in case the renamed library
// also depends on libraries).
LinkUpdateBundle bundle = getLinkBundle(projectState,
oldLibraries);
queueLinkUpdateBundle(bundle);
// add it to the opened projects to update whatever depends
// on it
if (projectState.isLibrary()) {
mOpenedLibraryProjects.add(projectState);
}
}
}
}
}
}
}
};
/**
* Delegate listener for file changes.
*/
private IFileListener mFileListener = new IFileListener() {
public void fileChanged(final IFile file, IMarkerDelta[] markerDeltas, int kind) {
if (SdkConstants.FN_DEFAULT_PROPERTIES.equals(file.getName()) &&
file.getParent() == file.getProject()) {
try {
// reload the content of the default.properties file and update
// the target.
IProject iProject = file.getProject();
if (iProject.hasNature(AndroidConstants.NATURE_DEFAULT) == false) {
return;
}
ProjectState state = Sdk.getProjectState(iProject);
// get the current target
IAndroidTarget oldTarget = state.getTarget();
// get the current library flag
boolean wasLibrary = state.isLibrary();
// get the current list of project dependencies
List<IProject> oldLibraries = state.getFullLibraryProjects();
LibraryDifference diff = state.reloadProperties();
// load the (possibly new) target.
IAndroidTarget newTarget = loadTarget(state);
// check if this is a new library
if (state.isLibrary() && wasLibrary == false) {
setupLibraryProject(iProject);
}
// reload the libraries if needed
if (diff.hasDiff()) {
if (diff.added) {
synchronized (sLock) {
for (ProjectState projectState : sProjectStateMap.values()) {
if (projectState != state) {
// need to call needs to do the libraryState link,
// but no need to look at the result, as we'll compare
// the result of getFullLibraryProjects()
// this is easier to due to indirect dependencies.
state.needs(projectState);
}
}
}
}
// and build the real difference. A list of new projects and a list of
// removed project.
// This is not the same as the added/removed libraries because libraries
// could be indirect dependencies through several different direct
// dependencies so it's easier to compare the full lists before and after
// the reload.
LinkUpdateBundle bundle = getLinkBundle(state, oldLibraries);
if (bundle != null) {
queueLinkUpdateBundle(bundle);
}
}
// apply the new target if needed.
if (newTarget != oldTarget) {
IJavaProject javaProject = BaseProjectHelper.getJavaProject(
file.getProject());
if (javaProject != null) {
AndroidClasspathContainerInitializer.updateProjects(
new IJavaProject[] { javaProject });
}
// update the editors to reload with the new target
AdtPlugin.getDefault().updateTargetListeners(iProject);
}
} catch (CoreException e) {
// This can't happen as it's only for closed project (or non existing)
// but in that case we can't get a fileChanged on this file.
}
}
}
};
/** List of opened project. This is filled in {@link IProjectListener#projectOpened(IProject)}
* and {@link IProjectListener#projectOpenedWithWorkspace(IProject)}, and processed in
* {@link IResourceEventListener#resourceChangeEventEnd()}.
*/
private final ArrayList<ProjectState> mOpenedLibraryProjects = new ArrayList<ProjectState>();
/**
* Delegate listener for resource changes. This is called before and after any calls to the
* project and file listeners (for a given resource change event).
*/
private IResourceEventListener mResourceEventListener = new IResourceEventListener() {
public void resourceChangeEventStart() {
// pass
}
public void resourceChangeEventEnd() {
updateProjectsWithNewLibraries(mOpenedLibraryProjects);
mOpenedLibraryProjects.clear();
}
};
/**
* Action bundle to update library links on a project.
*
* @see Sdk#queueLinkUpdateBundle(LinkUpdateBundle)
* @see Sdk#updateLibraryLinks(LinkUpdateBundle, IProgressMonitor)
*/
private static class LinkUpdateBundle {
/** The main project receiving the library links. */
IProject mProject = null;
/** A list (possibly null/empty) of projects that should be linked. */
IProject[] mNewLibraryProjects = null;
/** an optional old library path that needs to be removed at the same time as the new
* libraries are added. Can be <code>null</code> in which case no libraries are removed. */
IPath mDeletedLibraryPath = null;
/** A list (possibly null/empty) of projects that should be unlinked */
IProject[] mRemovedLibraryProjects = null;
/** Whether unknown IClasspathEntry (that were flagged as being added by ADT) are to be
* removed. This is typically only set to <code>true</code> when the project is opened. */
boolean mCleanupCPE = false;
@Override
public String toString() {
return String.format(
"LinkUpdateBundle: %1$s (clean: %2$s) > added: %3$s, removed: %4$s, deleted: %5$s", //$NON-NLS-1$
mProject.getName(),
mCleanupCPE,
Arrays.toString(mNewLibraryProjects),
Arrays.toString(mRemovedLibraryProjects),
mDeletedLibraryPath);
}
}
private final ArrayList<LinkUpdateBundle> mLinkActionBundleQueue =
new ArrayList<LinkUpdateBundle>();
/**
* Queues a {@link LinkUpdateBundle} bundle to be run by a job.
*
* All action bundles are executed in a job in the exact order they are added.
* This is convenient when several actions must be executed in a job consecutively (instead
* of in parallel as it would happen if each started its own job) but it is impossible
* to manually control the job that's running them (for instance each action is started from
* different callbacks such as {@link IProjectListener#projectOpened(IProject)}.
*
* If the job is not yet started, or has terminated due to lack of action bundle, it is
* restarted.
*
* @param bundle the action bundle to execute
*/
private void queueLinkUpdateBundle(LinkUpdateBundle bundle) {
boolean startJob = false;
synchronized (mLinkActionBundleQueue) {
startJob = mLinkActionBundleQueue.size() == 0;
mLinkActionBundleQueue.add(bundle);
}
if (startJob) {
Job job = new Job("Android Library Update") { //$NON-NLS-1$
@Override
protected IStatus run(IProgressMonitor monitor) {
// loop until there's no bundle to process
while (true) {
// get the bundle, but don't remove until we're done, or a new job could be
// started.
LinkUpdateBundle bundle = null;
synchronized (mLinkActionBundleQueue) {
// there is always a bundle at this point, as they are only removed
// at the end of this method, and the job is only started after adding
// one
bundle = mLinkActionBundleQueue.get(0);
}
// process the bundle.
try {
updateLibraryLinks(bundle, monitor);
} catch (Exception e) {
AdtPlugin.log(e, "Failed to process bundle: %1$s", //$NON-NLS-1$
bundle.toString());
}
try {
// force a recompile
bundle.mProject.build(IncrementalProjectBuilder.FULL_BUILD, monitor);
} catch (Exception e) {
// no need to log those.
}
// remove it from the list.
synchronized (mLinkActionBundleQueue) {
mLinkActionBundleQueue.remove(0);
// no more bundle to process? done.
if (mLinkActionBundleQueue.size() == 0) {
return Status.OK_STATUS;
}
}
}
}
};
job.setPriority(Job.BUILD);
job.schedule();
}
}
/**
* Adds to a list the resolved {@link IProject} dependencies for a given {@link ProjectState}.
* This recursively goes down to indirect dependencies.
*
* <strong>The list is filled in an order that is not valid for calling <code>aapt</code>
* </strong>.
* Use {@link ProjectState#getFullLibraryProjects()} for use with <code>aapt</code>.
*
* @param projectState the ProjectState of the project from which to add the libraries.
* @param libraries the list of {@link IProject} to fill.
*/
private void fillProjectDependenciesList(ProjectState projectState,
ArrayList<IProject> libraries) {
for (LibraryState libState : projectState.getLibraries()) {
ProjectState libProjectState = libState.getProjectState();
// only care if the LibraryState has a resolved ProjectState
if (libProjectState != null) {
// try not to add duplicate. This can happen if a project depends on 2 different
// libraries that both depend on the same one.
IProject libProject = libProjectState.getProject();
if (libraries.contains(libProject) == false) {
libraries.add(libProject);
}
// process the libraries of this library too.
fillProjectDependenciesList(libProjectState, libraries);
}
}
}
/**
* Sets up a path variable for a given project.
* The name of the variable is based on the name of the project. However some valid character
* for project names can be invalid for variable paths.
* {@link #getLibraryVariableName(String)} return the name of the variable based on the
* project name.
*
* @param libProject the project
*
* @see IPathVariableManager
* @see #getLibraryVariableName(String)
*/
private void setupLibraryProject(IProject libProject) {
// if needed add a path var for this library
IPathVariableManager pathVarMgr =
ResourcesPlugin.getWorkspace().getPathVariableManager();
IPath libPath = libProject.getLocation();
final String varName = getLibraryVariableName(libProject.getName());
if (libPath.equals(pathVarMgr.getValue(varName)) == false) {
try {
pathVarMgr.setValue(varName, libPath);
} catch (CoreException e) {
AdtPlugin.logAndPrintError(e, "Library Project",
"Unable to set linked path var '%1$s' for library %2$s: %3$s", //$NON-NLS-1$
varName, libPath.toOSString(), e.getMessage());
}
}
}
/**
* Deletes the path variable that was setup for the given project.
* @param project the project
* @see #disposeLibraryProject(String)
*/
private void disposeLibraryProject(IProject project) {
disposeLibraryProject(project.getName());
}
/**
* Deletes the path variable that was setup for the given project name.
* The name of the variable is based on the name of the project. However some valid character
* for project names can be invalid for variable paths.
* {@link #getLibraryVariableName(String)} return the name of the variable based on the
* project name.
* @param projectName the name of the project, unmodified.
*/
private void disposeLibraryProject(String projectName) {
IPathVariableManager pathVarMgr =
ResourcesPlugin.getWorkspace().getPathVariableManager();
final String varName = getLibraryVariableName(projectName);
// remove the value by setting the value to null.
try {
pathVarMgr.setValue(varName, null /*path*/);
} catch (CoreException e) {
String message = String.format("Unable to remove linked path var '%1$s'", //$NON-NLS-1$
varName);
AdtPlugin.log(e, message);
}
}
/**
* Returns a valid path variable name based on the name of a library project.
* @param name the name of the library project.
*/
private String getLibraryVariableName(String name) {
/*
* From the javadoc of IPathVariableManager:
* A path variable is a pair of non-null elements (name,value) where name is a
* case-sensitive string (containing only letters, digits and the underscore character,
* and not starting with a digit), and value is an absolute IPath object.
*/
// the variable name is made by:
// - prepending _android_ (this ensure there's no digit at the start)
// - removing all unsupported characters.
// - append the hashcode of the original name. This should help reduce collisions.
String validName = name.replaceAll("[^0-9a-zA-Z]+", "_"); //$NON-NLS-1$ //$NON-NLS-2$
//ensure the valid is not negative as - is not a valid char
long hash = name.hashCode() & 0x00000000ffffffffL;
return "_android_" + validName + "_" + Long.toString(hash, 16) ; //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* Update the library links for a project
*
* This does the follow:
* - add/remove the library projects to the main projects dynamic reference list. This is used
* by the builders to receive resource change deltas for library projects and figure out what
* needs to be recompiled/recreated.
* - create new {@link IClasspathEntry} of type {@link IClasspathEntry#CPE_SOURCE} for each
* source folder for each new library project.
* - remove the {@link IClasspathEntry} of type {@link IClasspathEntry#CPE_SOURCE} for each
* source folder for each removed library project.
* - If {@link LinkUpdateBundle#mCleanupCPE} is set to true, all CPE created by ADT that cannot
* be resolved are removed. This should only be used when the project is opened.
*
* <strong>This must not be called directly. Instead the {@link LinkUpdateBundle} must
* be run through a job with {@link #queueLinkUpdateBundle(LinkUpdateBundle)}.</strong>
*
* @param bundle The {@link LinkUpdateBundle} action bundle that contains all the parameters
* necessary to execute the action.
* @param monitor an {@link IProgressMonitor}.
* @return an {@link IStatus} with the status of the action.
*/
private IStatus updateLibraryLinks(LinkUpdateBundle bundle, IProgressMonitor monitor) {
if (bundle.mProject.isOpen() == false) {
return Status.OK_STATUS;
}
try {
// add the library to the list of dynamic references. This is necessary to receive
// notifications that the library content changed in the builders.
IProjectDescription projectDescription = bundle.mProject.getDescription();
IProject[] refs = projectDescription.getDynamicReferences();
if (refs.length > 0) {
ArrayList<IProject> list = new ArrayList<IProject>(Arrays.asList(refs));
// remove a previous library if needed (in case of a rename)
if (bundle.mDeletedLibraryPath != null) {
// since project basically have only one segment that matter,
// just check the names
removeFromList(list, bundle.mDeletedLibraryPath.lastSegment());
}
if (bundle.mRemovedLibraryProjects != null) {
for (IProject removedProject : bundle.mRemovedLibraryProjects) {
removeFromList(list, removedProject.getName());
}
}
// add the new ones if they don't exist
if (bundle.mNewLibraryProjects != null) {
for (IProject newProject : bundle.mNewLibraryProjects) {
if (list.contains(newProject) == false) {
list.add(newProject);
}
}
}
// set the changed list
projectDescription.setDynamicReferences(
list.toArray(new IProject[list.size()]));
} else {
if (bundle.mNewLibraryProjects != null) {
projectDescription.setDynamicReferences(bundle.mNewLibraryProjects);
}
}
// get the current classpath entries for the project to add the new source
// folders.
IJavaProject javaProject = JavaCore.create(bundle.mProject);
IClasspathEntry[] entries = javaProject.getRawClasspath();
ArrayList<IClasspathEntry> classpathEntries = new ArrayList<IClasspathEntry>(
Arrays.asList(entries));
IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot();
// loop on the classpath entries and look for CPE_SOURCE entries that
// are linked folders, then record them for comparison later as we add the new
// ones.
ArrayList<IClasspathEntry> cpeToRemove = new ArrayList<IClasspathEntry>();
for (IClasspathEntry classpathEntry : classpathEntries) {
if (classpathEntry.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
IPath path = classpathEntry.getPath();
IResource linkedRes = wsRoot.findMember(path);
if (linkedRes != null && linkedRes.isLinked() &&
CREATOR_ADT.equals(ProjectHelper.loadStringProperty(
linkedRes, PROP_CREATOR))) {
// add always to list if we're doing clean-up
if (bundle.mCleanupCPE) {
cpeToRemove.add(classpathEntry);
} else {
String libName = ProjectHelper.loadStringProperty(linkedRes,
PROP_LIBRARY_NAME);
if (libName != null && isRemovedLibrary(bundle, libName)) {
cpeToRemove.add(classpathEntry);
}
}
}
}
}
// loop on the projects to add.
if (bundle.mNewLibraryProjects != null) {
for (IProject library : bundle.mNewLibraryProjects) {
if (library.isOpen() == false) {
continue;
}
final String libName = library.getName();
final String varName = getLibraryVariableName(libName);
// get the list of source folders for the library.
List<IPath> sourceFolderPaths = BaseProjectHelper.getSourceClasspaths(library);
// loop on all the source folder, ignoring FD_GEN and add them
// as linked folder
for (IPath sourceFolderPath : sourceFolderPaths) {
IResource sourceFolder = wsRoot.findMember(sourceFolderPath);
if (sourceFolder == null || sourceFolder.isLinked()) {
continue;
}
IPath relativePath = sourceFolder.getProjectRelativePath();
if (SdkConstants.FD_GEN_SOURCES.equals(relativePath.toString())) {
continue;
}
// create the linked path
IPath linkedPath = new Path(varName).append(relativePath);
// look for an existing CPE that has the same linked path and that was
// going to be removed.
IClasspathEntry match = findClasspathEntryMatch(cpeToRemove, linkedPath,
null);
if (match == null) {
// no match, create one
// get a string version, to make up the linked folder name
String srcFolderName = relativePath.toString().replace(
"/", //$NON-NLS-1$
"_"); //$NON-NLS-1$
// folder name
String folderName = libName + "_" + srcFolderName; //$NON-NLS-1$
// create a linked resource for the library using the path var.
IFolder libSrc = bundle.mProject.getFolder(folderName);
IPath libSrcPath = libSrc.getFullPath();
// check if there's a CPE that would conflict, in which case it needs to
// be removed (this can happen for existing CPE that don't match an open
// project)
match = findClasspathEntryMatch(classpathEntries, null/*rawPath*/,
libSrcPath);
if (match != null) {
classpathEntries.remove(match);
}
// the path of the linked resource is based on the path variable
// representing the library project, followed by the source folder name.
libSrc.createLink(linkedPath, IResource.REPLACE, monitor);
// set some persistent properties on it to know that it was
// created by ADT.
ProjectHelper.saveStringProperty(libSrc, PROP_CREATOR, CREATOR_ADT);
ProjectHelper.saveResourceProperty(libSrc, PROP_LIBRARY, library);
ProjectHelper.saveStringProperty(libSrc, PROP_LIBRARY_NAME,
library.getName());
// add the source folder to the classpath entries
classpathEntries.add(JavaCore.newSourceEntry(libSrcPath));
} else {
// there's a valid match, do nothing, but remove the match from
// the list of previously existing CPE.
cpeToRemove.remove(match);
}
}
}
}
// remove the CPE that should be removed.
classpathEntries.removeAll(cpeToRemove);
// set the new list
javaProject.setRawClasspath(
classpathEntries.toArray(new IClasspathEntry[classpathEntries.size()]),
monitor);
// and delete the folders of the CPE that were removed (must be done after)
for (IClasspathEntry cpe : cpeToRemove) {
IResource res = wsRoot.findMember(cpe.getPath());
res.delete(true, monitor);
}
return Status.OK_STATUS;
} catch (CoreException e) {
AdtPlugin.logAndPrintError(e, bundle.mProject.getName(),
"Failed to create library links: %1$s", //$NON-NLS-1$
e.getMessage());
return e.getStatus();
}
}
private boolean isRemovedLibrary(LinkUpdateBundle bundle, String libName) {
if (bundle.mDeletedLibraryPath != null &&
libName.equals(bundle.mDeletedLibraryPath.lastSegment())) {
return true;
}
if (bundle.mRemovedLibraryProjects != null) {
for (IProject removedProject : bundle.mRemovedLibraryProjects) {
if (libName.equals(removedProject.getName())) {
return true;
}
}
}
return false;
}
/**
* Computes the library difference based on a previous list and a current state, and creates
* a {@link LinkUpdateBundle} action to update the given project.
* @param project The current project state
* @param oldLibraries the list of old libraries. Typically the result of
* {@link ProjectState#getFullLibraryProjects()} before the ProjectState is updated.
* @return null if there no action to take, or a {@link LinkUpdateBundle} object to run.
*/
private LinkUpdateBundle getLinkBundle(ProjectState project, List<IProject> oldLibraries) {
// get the new full list of projects
List<IProject> newLibraries = project.getFullLibraryProjects();
// and build the real difference. A list of new projects and a list of
// removed project.
// This is not the same as the added/removed libraries because libraries
// could be indirect dependencies through several different direct
// dependencies so it's easier to compare the full lists before and after
// the reload.
List<IProject> addedLibs = new ArrayList<IProject>();
List<IProject> removedLibs = new ArrayList<IProject>();
// first get the list of new projects.
for (IProject newLibrary : newLibraries) {
boolean found = false;
for (IProject oldLibrary : oldLibraries) {
if (newLibrary.equals(oldLibrary)) {
found = true;
break;
}
}
// if it was not found in the old libraries, it's really new
if (found == false) {
addedLibs.add(newLibrary);
}
}
// now the list of removed projects.
for (IProject oldLibrary : oldLibraries) {
boolean found = false;
for (IProject newLibrary : newLibraries) {
if (newLibrary.equals(oldLibrary)) {
found = true;
break;
}
}
// if it was not found in the new libraries, it's really been removed
if (found == false) {
removedLibs.add(oldLibrary);
}
}
if (addedLibs.size() > 0 || removedLibs.size() > 0) {
LinkUpdateBundle bundle = new LinkUpdateBundle();
bundle.mProject = project.getProject();
bundle.mNewLibraryProjects =
addedLibs.toArray(new IProject[addedLibs.size()]);
bundle.mRemovedLibraryProjects =
removedLibs.toArray(new IProject[removedLibs.size()]);
return bundle;
}
return null;
}
/**
* Removes a project from a list based on its name.
* @param projects the list of projects.
* @param name the name of the project to remove.
*/
private void removeFromList(List<IProject> projects, String name) {
final int count = projects.size();
for (int i = 0 ; i < count ; i++) {
// since project basically have only one segment that matter,
// just check the names
if (projects.get(i).getName().equals(name)) {
projects.remove(i);
return;
}
}
}
/**
* Returns a {@link IClasspathEntry} from the given list whose linked path match the given path.
* @param cpeList a list of {@link IClasspathEntry} of {@link IClasspathEntry#getEntryKind()}
* {@link IClasspathEntry#CPE_SOURCE} whose {@link IClasspathEntry#getPath()}
* points to a linked folder.
* @param rawPath the raw path to compare to. Can be null if <var>path</var> is used instead.
* @param path the path to compare to. Can be null if <var>rawPath</var> is used instead.
* @return the matching IClasspathEntry or null.
*/
private IClasspathEntry findClasspathEntryMatch(ArrayList<IClasspathEntry> cpeList,
IPath rawPath, IPath path) {
IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot();
for (IClasspathEntry cpe : cpeList) {
IPath cpePath = cpe.getPath();
// test the normal path of the resource.
if (path != null && path.equals(cpePath)) {
return cpe;
}
IResource res = wsRoot.findMember(cpePath);
// getRawLocation returns the path that the linked folder points to.
if (rawPath != null && res.getRawLocation().equals(rawPath)) {
return cpe;
}
}
return null;
}
/**
* Updates all existing projects with a given list of new/updated libraries.
* This loops through all opened projects and check if they depend on any of the given
* library project, and if they do, they are linked together.
* @param libraries the list of new/updated library projects.
*/
private void updateProjectsWithNewLibraries(List<ProjectState> libraries) {
if (libraries.size() == 0) {
return;
}
ArrayList<ProjectState> updatedLibraries = new ArrayList<ProjectState>();
synchronized (sLock) {
// for each projects, look for projects that depend on it, and update them.
// Once they are updated (meaning ProjectState#needs() has been called on them),
// we add them to the list so that can be updated as well.
for (ProjectState projectState : sProjectStateMap.values()) {
// record the current library dependencies
List<IProject> oldLibraries = projectState.getFullLibraryProjects();
boolean needLibraryDependenciesUpdated = false;
for (ProjectState library : libraries) {
// Normally we would only need to test if ProjectState#needs returns non null,
// meaning the link between the project and the library has not been
// done yet.
// However what matters here is that the library is a dependency,
// period. If the library project was updated, then we redo the link,
// with all indirect dependencies (which *have* changed, since this is
// what this method is all about.)
// We still need to call ProjectState#needs to make the link in case it's not
// been done yet (which can happen if the library project was just opened).
if (projectState != library) {
// call needs in case this new library was just opened, and the link needs
// to be done
LibraryState libState = projectState.needs(library);
if (libState == null && projectState.dependsOn(library)) {
// ProjectState.needs only returns true if the library was needed.
// but we also need to check the case where the project depends on
// the library but the link was already done.
needLibraryDependenciesUpdated = true;
}
}
}
if (needLibraryDependenciesUpdated) {
projectState.updateFullLibraryList();
}
LinkUpdateBundle bundle = getLinkBundle(projectState, oldLibraries);
if (bundle != null) {
queueLinkUpdateBundle(bundle);
// if this updated project is a library, add it to the list, so that
// projects depending on it get updated too.
if (projectState.isLibrary() &&
updatedLibraries.contains(projectState) == false) {
updatedLibraries.add(projectState);
}
}
}
}
// done, but there may be updated projects that were libraries, so we need to do the same
// for this libraries, to update the project there were depending on.
updateProjectsWithNewLibraries(updatedLibraries);
}
}