2 * Copyright (C) 2008 The Android Open Source Project
4 * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.eclipse.org/org/documents/epl-v10.php
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.android.ide.eclipse.adt.internal.sdk;
19 import com.android.ddmlib.IDevice;
20 import com.android.ide.eclipse.adt.AdtPlugin;
21 import com.android.ide.eclipse.adt.internal.project.AndroidClasspathContainerInitializer;
22 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
23 import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
24 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor;
25 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener;
26 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IProjectListener;
27 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IResourceEventListener;
28 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData.LayoutBridge;
29 import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryDifference;
30 import com.android.ide.eclipse.adt.internal.sdk.ProjectState.LibraryState;
31 import com.android.prefs.AndroidLocation.AndroidLocationException;
32 import com.android.sdklib.AndroidVersion;
33 import com.android.sdklib.IAndroidTarget;
34 import com.android.sdklib.ISdkLog;
35 import com.android.sdklib.SdkConstants;
36 import com.android.sdklib.SdkManager;
37 import com.android.sdklib.internal.avd.AvdManager;
38 import com.android.sdklib.internal.project.ProjectProperties;
39 import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy;
40 import com.android.sdklib.internal.project.ProjectProperties.PropertyType;
41 import com.android.sdklib.io.StreamException;
43 import org.eclipse.core.resources.IFile;
44 import org.eclipse.core.resources.IFolder;
45 import org.eclipse.core.resources.IMarkerDelta;
46 import org.eclipse.core.resources.IPathVariableManager;
47 import org.eclipse.core.resources.IProject;
48 import org.eclipse.core.resources.IProjectDescription;
49 import org.eclipse.core.resources.IResource;
50 import org.eclipse.core.resources.IResourceDelta;
51 import org.eclipse.core.resources.IWorkspaceRoot;
52 import org.eclipse.core.resources.IncrementalProjectBuilder;
53 import org.eclipse.core.resources.ResourcesPlugin;
54 import org.eclipse.core.runtime.CoreException;
55 import org.eclipse.core.runtime.IPath;
56 import org.eclipse.core.runtime.IProgressMonitor;
57 import org.eclipse.core.runtime.IStatus;
58 import org.eclipse.core.runtime.Path;
59 import org.eclipse.core.runtime.Status;
60 import org.eclipse.core.runtime.jobs.Job;
61 import org.eclipse.jdt.core.IClasspathEntry;
62 import org.eclipse.jdt.core.IJavaProject;
63 import org.eclipse.jdt.core.JavaCore;
66 import java.io.IOException;
67 import java.net.MalformedURLException;
69 import java.util.ArrayList;
70 import java.util.Arrays;
71 import java.util.HashMap;
72 import java.util.HashSet;
73 import java.util.List;
76 import java.util.Map.Entry;
79 * Central point to load, manipulate and deal with the Android SDK. Only one SDK can be used
82 * To start using an SDK, call {@link #loadSdk(String)} which returns the instance of
85 * To get the list of platforms or add-ons present in the SDK, call {@link #getTargets()}.
87 public final class Sdk {
88 private static final String PROP_LIBRARY = "_library"; //$NON-NLS-1$
89 private static final String PROP_LIBRARY_NAME = "_library_name"; //$NON-NLS-1$
90 public static final String CREATOR_ADT = "ADT"; //$NON-NLS-1$
91 public static final String PROP_CREATOR = "_creator"; //$NON-NLS-1$
92 private final static Object sLock = new Object();
94 private static Sdk sCurrentSdk = null;
97 * Map associating {@link IProject} and their state {@link ProjectState}.
98 * <p/>This <b>MUST NOT</b> be accessed directly. Instead use {@link #getProjectState(IProject)}.
100 private final static HashMap<IProject, ProjectState> sProjectStateMap =
101 new HashMap<IProject, ProjectState>();
104 * Data bundled using during the load of Target data.
105 * <p/>This contains the {@link LoadStatus} and a list of projects that attempted
106 * to compile before the loading was finished. Those projects will be recompiled
107 * at the end of the loading.
109 private final static class TargetLoadBundle {
111 final HashSet<IJavaProject> projecsToReload = new HashSet<IJavaProject>();
114 private final SdkManager mManager;
115 private final AvdManager mAvdManager;
117 /** Map associating an {@link IAndroidTarget} to an {@link AndroidTargetData} */
118 private final HashMap<IAndroidTarget, AndroidTargetData> mTargetDataMap =
119 new HashMap<IAndroidTarget, AndroidTargetData>();
120 /** Map associating an {@link IAndroidTarget} and its {@link TargetLoadBundle}. */
121 private final HashMap<IAndroidTarget, TargetLoadBundle> mTargetDataStatusMap =
122 new HashMap<IAndroidTarget, TargetLoadBundle>();
124 private final String mDocBaseUrl;
126 private final LayoutDeviceManager mLayoutDeviceManager = new LayoutDeviceManager();
129 * Classes implementing this interface will receive notification when targets are changed.
131 public interface ITargetChangeListener {
133 * Sent when project has its target changed.
135 void onProjectTargetChange(IProject changedProject);
138 * Called when the targets are loaded (either the SDK finished loading when Eclipse starts,
139 * or the SDK is changed).
141 void onTargetLoaded(IAndroidTarget target);
144 * Called when the base content of the SDK is parsed.
150 * Basic abstract implementation of the ITargetChangeListener for the case where both
151 * {@link #onProjectTargetChange(IProject)} and {@link #onTargetLoaded(IAndroidTarget)}
152 * use the same code based on a simple test requiring to know the current IProject.
154 public static abstract class TargetChangeListener implements ITargetChangeListener {
156 * Returns the {@link IProject} associated with the listener.
158 public abstract IProject getProject();
161 * Called when the listener needs to take action on the event. This is only called
162 * if {@link #getProject()} and the {@link IAndroidTarget} associated with the project
163 * match the values received in {@link #onProjectTargetChange(IProject)} and
164 * {@link #onTargetLoaded(IAndroidTarget)}.
166 public abstract void reload();
168 public void onProjectTargetChange(IProject changedProject) {
169 if (changedProject != null && changedProject.equals(getProject())) {
174 public void onTargetLoaded(IAndroidTarget target) {
175 IProject project = getProject();
176 if (target != null && target.equals(Sdk.getCurrent().getTarget(project))) {
181 public void onSdkLoaded() {
187 * Returns the lock object used to synchronize all operations dealing with SDK, targets and
190 public static final Object getLock() {
195 * Loads an SDK and returns an {@link Sdk} object if success.
196 * <p/>If the SDK failed to load, it displays an error to the user.
197 * @param sdkLocation the OS path to the SDK.
199 public static Sdk loadSdk(String sdkLocation) {
200 synchronized (sLock) {
201 if (sCurrentSdk != null) {
202 sCurrentSdk.dispose();
206 final ArrayList<String> logMessages = new ArrayList<String>();
207 ISdkLog log = new ISdkLog() {
208 public void error(Throwable throwable, String errorFormat, Object... arg) {
209 if (errorFormat != null) {
210 logMessages.add(String.format("Error: " + errorFormat, arg));
213 if (throwable != null) {
214 logMessages.add(throwable.getMessage());
218 public void warning(String warningFormat, Object... arg) {
219 logMessages.add(String.format("Warning: " + warningFormat, arg));
222 public void printf(String msgFormat, Object... arg) {
223 logMessages.add(String.format(msgFormat, arg));
227 // get an SdkManager object for the location
228 SdkManager manager = SdkManager.createManager(sdkLocation, log);
229 if (manager != null) {
230 AvdManager avdManager = null;
232 avdManager = new AvdManager(manager, log);
233 } catch (AndroidLocationException e) {
234 log.error(e, "Error parsing the AVDs");
236 sCurrentSdk = new Sdk(manager, avdManager);
239 StringBuilder sb = new StringBuilder("Error Loading the SDK:\n");
240 for (String msg : logMessages) {
244 AdtPlugin.displayError("Android SDK", sb.toString());
251 * Returns the current {@link Sdk} object.
253 public static Sdk getCurrent() {
254 synchronized (sLock) {
260 * Returns the location (OS path) of the current SDK.
262 public String getSdkLocation() {
263 return mManager.getLocation();
267 * Returns the URL to the local documentation.
268 * Can return null if no documentation is found in the current SDK.
270 * @return A file:// URL on the local documentation folder if it exists or null.
272 public String getDocumentationBaseUrl() {
277 * Returns the list of targets that are available in the SDK.
279 public IAndroidTarget[] getTargets() {
280 return mManager.getTargets();
284 * Returns a target from a hash that was generated by {@link IAndroidTarget#hashString()}.
286 * @param hash the {@link IAndroidTarget} hash string.
287 * @return The matching {@link IAndroidTarget} or null.
289 public IAndroidTarget getTargetFromHashString(String hash) {
290 return mManager.getTargetFromHashString(hash);
294 * Initializes a new project with a target. This creates the <code>default.properties</code>
296 * @param project the project to intialize
297 * @param target the project's target.
298 * @throws IOException if creating the file failed in any way.
299 * @throws StreamException
301 public void initProject(IProject project, IAndroidTarget target)
302 throws IOException, StreamException {
303 if (project == null || target == null) {
307 synchronized (sLock) {
308 // check if there's already a state?
309 ProjectState state = getProjectState(project);
311 ProjectPropertiesWorkingCopy properties = null;
314 properties = state.getProperties().makeWorkingCopy();
317 if (properties == null) {
318 IPath location = project.getLocation();
319 if (location == null) { // can return null when the project is being deleted.
320 // do nothing and return null;
324 properties = ProjectProperties.create(location.toOSString(), PropertyType.DEFAULT);
327 // save the target hash string in the project persistent property
328 properties.setProperty(ProjectProperties.PROPERTY_TARGET, target.hashString());
334 * Returns the {@link ProjectState} object associated with a given project.
336 * This method is the only way to properly get the project's {@link ProjectState}
337 * If the project has not yet been loaded, then it is loaded.
338 * <p/>Because this methods deals with projects, it's not linked to an actual {@link Sdk}
339 * objects, and therefore is static.
340 * <p/>The value returned by {@link ProjectState#getTarget()} will change as {@link Sdk} objects
342 * @param project the request project
343 * @return the ProjectState for the project.
345 public static ProjectState getProjectState(IProject project) {
346 if (project == null) {
350 synchronized (sLock) {
351 ProjectState state = sProjectStateMap.get(project);
353 // load the default.properties from the project folder.
354 IPath location = project.getLocation();
355 if (location == null) { // can return null when the project is being deleted.
356 // do nothing and return null;
360 ProjectProperties properties = ProjectProperties.load(location.toOSString(),
361 PropertyType.DEFAULT);
362 if (properties == null) {
363 AdtPlugin.log(IStatus.ERROR, "Failed to load properties file for project '%s'",
368 state = new ProjectState(project, properties);
369 sProjectStateMap.put(project, state);
371 // try to resolve the target
372 if (AdtPlugin.getDefault().getSdkLoadStatus() == LoadStatus.LOADED) {
373 sCurrentSdk.loadTarget(state);
382 * Returns the {@link IAndroidTarget} object associated with the given {@link IProject}.
384 public IAndroidTarget getTarget(IProject project) {
385 if (project == null) {
389 ProjectState state = getProjectState(project);
391 return state.getTarget();
398 * Loads the {@link IAndroidTarget} for a given project.
399 * <p/>This method will get the target hash string from the project properties, and resolve
400 * it to an {@link IAndroidTarget} object and store it inside the {@link ProjectState}.
401 * @param state the state representing the project to load.
402 * @return the target that was loaded.
404 public IAndroidTarget loadTarget(ProjectState state) {
405 IAndroidTarget target = null;
406 String hash = state.getTargetHashString();
408 state.setTarget(target = getTargetFromHashString(hash));
415 * Checks and loads (if needed) the data for a given target.
416 * <p/> The data is loaded in a separate {@link Job}, and opened editors will be notified
417 * through their implementation of {@link ITargetChangeListener#onTargetLoaded(IAndroidTarget)}.
418 * <p/>An optional project as second parameter can be given to be recompiled once the target
419 * data is finished loading.
420 * <p/>The return value is non-null only if the target data has already been loaded (and in this
421 * case is the status of the load operation)
422 * @param target the target to load.
423 * @param project an optional project to be recompiled when the target data is loaded.
424 * If the target is already loaded, nothing happens.
425 * @return The load status if the target data is already loaded.
427 public LoadStatus checkAndLoadTargetData(final IAndroidTarget target, IJavaProject project) {
428 boolean loadData = false;
430 synchronized (sLock) {
431 TargetLoadBundle bundle = mTargetDataStatusMap.get(target);
432 if (bundle == null) {
433 bundle = new TargetLoadBundle();
434 mTargetDataStatusMap.put(target,bundle);
436 // set status to loading
437 bundle.status = LoadStatus.LOADING;
439 // add project to bundle
440 if (project != null) {
441 bundle.projecsToReload.add(project);
444 // and set the flag to start the loading below
446 } else if (bundle.status == LoadStatus.LOADING) {
447 // add project to bundle
448 if (project != null) {
449 bundle.projecsToReload.add(project);
452 return bundle.status;
453 } else if (bundle.status == LoadStatus.LOADED || bundle.status == LoadStatus.FAILED) {
454 return bundle.status;
459 Job job = new Job(String.format("Loading data for %1$s", target.getFullName())) {
461 protected IStatus run(IProgressMonitor monitor) {
462 AdtPlugin plugin = AdtPlugin.getDefault();
464 IStatus status = new AndroidTargetParser(target).run(monitor);
466 IJavaProject[] javaProjectArray = null;
468 synchronized (sLock) {
469 TargetLoadBundle bundle = mTargetDataStatusMap.get(target);
471 if (status.getCode() != IStatus.OK) {
472 bundle.status = LoadStatus.FAILED;
473 bundle.projecsToReload.clear();
475 bundle.status = LoadStatus.LOADED;
477 // Prepare the array of project to recompile.
478 // The call is done outside of the synchronized block.
479 javaProjectArray = bundle.projecsToReload.toArray(
480 new IJavaProject[bundle.projecsToReload.size()]);
482 // and update the UI of the editors that depend on the target data.
483 plugin.updateTargetListeners(target);
487 if (javaProjectArray != null) {
488 AndroidClasspathContainerInitializer.updateProjects(javaProjectArray);
492 } catch (Throwable t) {
493 synchronized (sLock) {
494 TargetLoadBundle bundle = mTargetDataStatusMap.get(target);
495 bundle.status = LoadStatus.FAILED;
498 AdtPlugin.log(t, "Exception in checkAndLoadTargetData."); //$NON-NLS-1$
499 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
501 "Parsing Data for %1$s failed", //$NON-NLS-1$
502 target.hashString()),
507 job.setPriority(Job.BUILD); // build jobs are run after other interactive jobs
511 // The only way to go through here is when the loading starts through the Job.
512 // Therefore the current status of the target is LOADING.
513 return LoadStatus.LOADING;
517 * Return the {@link AndroidTargetData} for a given {@link IAndroidTarget}.
519 public AndroidTargetData getTargetData(IAndroidTarget target) {
520 synchronized (sLock) {
521 return mTargetDataMap.get(target);
526 * Return the {@link AndroidTargetData} for a given {@link IProject}.
528 public AndroidTargetData getTargetData(IProject project) {
529 synchronized (sLock) {
530 IAndroidTarget target = getTarget(project);
531 if (target != null) {
532 return getTargetData(target);
540 * Returns the {@link AvdManager}. If the AvdManager failed to parse the AVD folder, this could
541 * be <code>null</code>.
543 public AvdManager getAvdManager() {
547 public static AndroidVersion getDeviceVersion(IDevice device) {
549 Map<String, String> props = device.getProperties();
550 String apiLevel = props.get(IDevice.PROP_BUILD_API_LEVEL);
551 if (apiLevel == null) {
555 return new AndroidVersion(Integer.parseInt(apiLevel),
556 props.get((IDevice.PROP_BUILD_CODENAME)));
557 } catch (NumberFormatException e) {
562 public LayoutDeviceManager getLayoutDeviceManager() {
563 return mLayoutDeviceManager;
567 * Returns a list of {@link ProjectState} representing projects depending, directly or
568 * indirectly on a given library project.
569 * @param project the library project.
570 * @return a possibly empty list of ProjectState.
572 public static Set<ProjectState> getMainProjectsFor(IProject project) {
573 synchronized (sLock) {
574 // first get the project directly depending on this.
575 HashSet<ProjectState> list = new HashSet<ProjectState>();
577 // loop on all project and see if ProjectState.getLibrary returns a non null
579 for (Entry<IProject, ProjectState> entry : sProjectStateMap.entrySet()) {
580 if (project != entry.getKey()) {
581 LibraryState library = entry.getValue().getLibrary(project);
582 if (library != null) {
583 list.add(entry.getValue());
588 // now look for projects depending on the projects directly depending on the library.
589 HashSet<ProjectState> result = new HashSet<ProjectState>(list);
590 for (ProjectState p : list) {
592 Set<ProjectState> set = getMainProjectsFor(p.getProject());
601 private Sdk(SdkManager manager, AvdManager avdManager) {
603 mAvdManager = avdManager;
605 // listen to projects closing
606 GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor();
607 monitor.addProjectListener(mProjectListener);
608 monitor.addFileListener(mFileListener, IResourceDelta.CHANGED | IResourceDelta.ADDED);
609 monitor.addResourceEventListener(mResourceEventListener);
611 // pre-compute some paths
612 mDocBaseUrl = getDocumentationBaseUrl(mManager.getLocation() +
613 SdkConstants.OS_SDK_DOCS_FOLDER);
615 // load the built-in and user layout devices
616 mLayoutDeviceManager.loadDefaultAndUserDevices(mManager.getLocation());
617 // and the ones from the add-on
620 // update whatever ProjectState is already present with new IAndroidTarget objects.
621 synchronized (sLock) {
622 for (Entry<IProject, ProjectState> entry: sProjectStateMap.entrySet()) {
623 entry.getValue().setTarget(
624 getTargetFromHashString(entry.getValue().getTargetHashString()));
630 * Cleans and unloads the SDK.
632 private void dispose() {
633 GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor();
634 monitor.removeProjectListener(mProjectListener);
635 monitor.removeFileListener(mFileListener);
636 monitor.removeResourceEventListener(mResourceEventListener);
638 // the IAndroidTarget objects are now obsolete so update the project states.
639 synchronized (sLock) {
640 for (Entry<IProject, ProjectState> entry: sProjectStateMap.entrySet()) {
641 entry.getValue().setTarget(null);
646 void setTargetData(IAndroidTarget target, AndroidTargetData data) {
647 synchronized (sLock) {
648 mTargetDataMap.put(target, data);
653 * Returns the URL to the local documentation.
654 * Can return null if no documentation is found in the current SDK.
656 * @param osDocsPath Path to the documentation folder in the current SDK.
657 * The folder may not actually exist.
658 * @return A file:// URL on the local documentation folder if it exists or null.
660 private String getDocumentationBaseUrl(String osDocsPath) {
661 File f = new File(osDocsPath);
663 if (f.isDirectory()) {
665 // Note: to create a file:// URL, one would typically use something like
666 // f.toURI().toURL().toString(). However this generates a broken path on
667 // Windows, namely "C:\\foo" is converted to "file:/C:/foo" instead of
668 // "file:///C:/foo" (i.e. there should be 3 / after "file:"). So we'll
669 // do the correct thing manually.
671 String path = f.getAbsolutePath();
672 if (File.separatorChar != '/') {
673 path = path.replace(File.separatorChar, '/');
676 // For some reason the URL class doesn't add the mandatory "//" after
677 // the "file:" protocol name, so it has to be hacked into the path.
678 URL url = new URL("file", null, "//" + path); //$NON-NLS-1$ //$NON-NLS-2$
679 String result = url.toString();
681 } catch (MalformedURLException e) {
682 // ignore malformed URLs
690 * Parses the SDK add-ons to look for files called {@link SdkConstants#FN_DEVICES_XML} to
691 * load {@link LayoutDevice} from them.
693 private void loadLayoutDevices() {
694 IAndroidTarget[] targets = mManager.getTargets();
695 for (IAndroidTarget target : targets) {
696 if (target.isPlatform() == false) {
697 File deviceXml = new File(target.getLocation(), SdkConstants.FN_DEVICES_XML);
698 if (deviceXml.isFile()) {
699 mLayoutDeviceManager.parseAddOnLayoutDevice(deviceXml);
704 mLayoutDeviceManager.sealAddonLayoutDevices();
708 * Delegate listener for project changes.
710 private IProjectListener mProjectListener = new IProjectListener() {
711 public void projectClosed(IProject project) {
712 onProjectRemoved(project, false /*deleted*/);
715 public void projectDeleted(IProject project) {
716 onProjectRemoved(project, true /*deleted*/);
719 private void onProjectRemoved(IProject project, boolean deleted) {
720 // get the target project
721 synchronized (sLock) {
722 // Don't use getProject() as it could create the ProjectState if it's not
723 // there yet and this is not what we want. We want the current object.
724 // Therefore, direct access to the map.
725 ProjectState state = sProjectStateMap.get(project);
727 // 1. clear the layout lib cache associated with this project
728 IAndroidTarget target = state.getTarget();
729 if (target != null) {
730 // get the bridge for the target, and clear the cache for this project.
731 AndroidTargetData data = mTargetDataMap.get(target);
733 LayoutBridge bridge = data.getLayoutBridge();
734 if (bridge != null && bridge.status == LoadStatus.LOADED) {
735 bridge.bridge.clearCaches(project);
740 // 2. if the project is a library, make sure to update the
741 // LibraryState for any main project using this.
742 // Also, record the updated projects that are libraries, to update
743 // projects that depend on them.
744 ArrayList<ProjectState> updatedLibraries = new ArrayList<ProjectState>();
745 for (ProjectState projectState : sProjectStateMap.values()) {
746 LibraryState libState = projectState.getLibrary(project);
747 if (libState != null) {
748 // get the current libraries.
749 IProject[] oldLibraries = projectState.getFullLibraryProjects();
751 // the unlink below will work in the job, but we need to close
752 // the library right away.
753 // This is because in case of a rename of a project, projectClosed and
754 // projectOpened will be called before any other job is run, so we
755 // need to make sure projectOpened is closed with the main project
760 // edit the project to remove the linked source folder.
761 // this also calls LibraryState.close();
762 LinkUpdateBundle bundle = getLinkBundle(projectState, oldLibraries);
763 if (bundle != null) {
764 queueLinkUpdateBundle(bundle);
767 if (projectState.isLibrary()) {
768 updatedLibraries.add(projectState);
774 // remove the linked path variable
775 disposeLibraryProject(project);
778 // now remove the project for the project map.
779 sProjectStateMap.remove(project);
781 // update the projects that depend on the updated project
782 updateProjectsWithNewLibraries(updatedLibraries);
787 public void projectOpened(IProject project) {
788 onProjectOpened(project);
791 public void projectOpenedWithWorkspace(IProject project) {
792 // no need to force recompilation when projects are opened with the workspace.
793 onProjectOpened(project);
796 private void onProjectOpened(final IProject openedProject) {
797 ProjectState openedState = getProjectState(openedProject);
798 if (openedState != null) {
799 if (openedState.hasLibraries()) {
800 // list of library to link to the opened project.
801 final ArrayList<IProject> libsToLink = new ArrayList<IProject>();
803 // Look for all other opened projects to see if any is a library for the opened
805 synchronized (sLock) {
806 for (ProjectState projectState : sProjectStateMap.values()) {
807 if (projectState != openedState) {
808 // ProjectState#needs() both checks if this is a missing library
809 // and updates LibraryState to contains the new values.
810 LibraryState libState = openedState.needs(projectState);
812 if (libState != null) {
813 // we have a match! Add the library to the list (if it was
814 // not added through an indirect dependency before).
815 IProject libProject = libState.getProjectState().getProject();
816 if (libsToLink.contains(libProject) == false) {
817 libsToLink.add(libProject);
820 // now find what this depends on, and add it too.
821 // The order here doesn't matter
822 // as it's just to add the linked source folder, so there's no
823 // need to use ProjectState#getFullLibraryProjects() which
824 // could return project that have already been added anyway.
825 fillProjectDependenciesList(libState.getProjectState(),
832 // create a link bundle always, because even if there's no libraries to add
833 // to the CPE, the cleaning of invalid CPE must happen.
834 LinkUpdateBundle bundle = new LinkUpdateBundle();
835 bundle.mProject = openedProject;
836 bundle.mNewLibraryProjects = libsToLink.toArray(
837 new IProject[libsToLink.size()]);
838 bundle.mCleanupCPE = true;
839 queueLinkUpdateBundle(bundle);
842 // if the project is a library, then add it to the list of projects being opened.
843 // They will be processed in IResourceEventListener#resourceChangeEventEnd.
844 // This is done so that we are sure to process all the projects being opened
845 // first and only then process projects depending on the projects that were opened.
846 if (openedState.isLibrary()) {
847 setupLibraryProject(openedProject);
849 mOpenedLibraryProjects.add(openedState);
854 public void projectRenamed(IProject project, IPath from) {
855 System.out.println("RENAMED: " + project);
856 // a project was renamed.
857 // if the project is a library, look for any project that depended on it
858 // and update it. (default.properties and linked source folder)
859 ProjectState renamedState = getProjectState(project);
860 if (renamedState.isLibrary()) {
861 // remove the variable
862 disposeLibraryProject(from.lastSegment());
864 // update the project depending on the library
865 synchronized (sLock) {
866 for (ProjectState projectState : sProjectStateMap.values()) {
867 if (projectState != renamedState && projectState.isMissingLibraries()) {
868 IPath oldRelativePath = makeRelativeTo(from,
869 projectState.getProject().getFullPath());
871 IPath newRelativePath = makeRelativeTo(project.getFullPath(),
872 projectState.getProject().getFullPath());
874 // get the current libraries
875 IProject[] oldLibraries = projectState.getFullLibraryProjects();
877 // update the library for the main project.
878 LibraryState libState = projectState.updateLibrary(
879 oldRelativePath.toString(), newRelativePath.toString(),
881 if (libState != null) {
882 // this project depended on the renamed library, create a bundle
883 // with the whole library difference (in case the renamed library
884 // also depends on libraries).
886 LinkUpdateBundle bundle = getLinkBundle(projectState,
888 queueLinkUpdateBundle(bundle);
890 // add it to the opened projects to update whatever depends
892 if (projectState.isLibrary()) {
893 mOpenedLibraryProjects.add(projectState);
904 * Delegate listener for file changes.
906 private IFileListener mFileListener = new IFileListener() {
907 public void fileChanged(final IFile file, IMarkerDelta[] markerDeltas, int kind) {
908 if (SdkConstants.FN_DEFAULT_PROPERTIES.equals(file.getName()) &&
909 file.getParent() == file.getProject()) {
911 // reload the content of the default.properties file and update
913 IProject iProject = file.getProject();
914 ProjectState state = Sdk.getProjectState(iProject);
916 // get the current target
917 IAndroidTarget oldTarget = state.getTarget();
919 // get the current library flag
920 boolean wasLibrary = state.isLibrary();
922 // get the current list of project dependencies
923 IProject[] oldLibraries = state.getFullLibraryProjects();
925 LibraryDifference diff = state.reloadProperties();
927 // load the (possibly new) target.
928 IAndroidTarget newTarget = loadTarget(state);
930 // check if this is a new library
931 if (state.isLibrary() && wasLibrary == false) {
932 setupLibraryProject(iProject);
935 // reload the libraries if needed
936 if (diff.hasDiff()) {
938 synchronized (sLock) {
939 for (ProjectState projectState : sProjectStateMap.values()) {
940 if (projectState != state) {
941 // need to call needs to do the libraryState link,
942 // but no need to look at the result, as we'll compare
943 // the result of getFullLibraryProjects()
944 // this is easier to due to indirect dependencies.
945 state.needs(projectState);
951 // and build the real difference. A list of new projects and a list of
953 // This is not the same as the added/removed libraries because libraries
954 // could be indirect dependencies through several different direct
955 // dependencies so it's easier to compare the full lists before and after
957 LinkUpdateBundle bundle = getLinkBundle(state, oldLibraries);
958 if (bundle != null) {
959 queueLinkUpdateBundle(bundle);
963 // apply the new target if needed.
964 if (newTarget != oldTarget) {
965 IJavaProject javaProject = BaseProjectHelper.getJavaProject(
967 if (javaProject != null) {
968 AndroidClasspathContainerInitializer.updateProjects(
969 new IJavaProject[] { javaProject });
972 // update the editors to reload with the new target
973 AdtPlugin.getDefault().updateTargetListeners(iProject);
975 } catch (CoreException e) {
976 // This can't happen as it's only for closed project (or non existing)
977 // but in that case we can't get a fileChanged on this file.
983 /** List of opened project. This is filled in {@link IProjectListener#projectOpened(IProject)}
984 * and {@link IProjectListener#projectOpenedWithWorkspace(IProject)}, and processed in
985 * {@link IResourceEventListener#resourceChangeEventEnd()}.
987 private final ArrayList<ProjectState> mOpenedLibraryProjects = new ArrayList<ProjectState>();
990 * Delegate listener for resource changes. This is called before and after any calls to the
991 * project and file listeners (for a given resource change event).
993 private IResourceEventListener mResourceEventListener = new IResourceEventListener() {
994 public void resourceChangeEventStart() {
998 public void resourceChangeEventEnd() {
999 updateProjectsWithNewLibraries(mOpenedLibraryProjects);
1000 mOpenedLibraryProjects.clear();
1005 * Action bundle to update library links on a project.
1007 * @see Sdk#queueLinkUpdateBundle(LinkUpdateBundle)
1008 * @see Sdk#updateLibraryLinks(LinkUpdateBundle, IProgressMonitor)
1010 private static class LinkUpdateBundle {
1012 /** The main project receiving the library links. */
1013 IProject mProject = null;
1014 /** A list (possibly null/empty) of projects that should be linked. */
1015 IProject[] mNewLibraryProjects = null;
1016 /** an optional old library path that needs to be removed at the same time as the new
1017 * libraries are added. Can be <code>null</code> in which case no libraries are removed. */
1018 IPath mDeletedLibraryPath = null;
1019 /** A list (possibly null/empty) of projects that should be unlinked */
1020 IProject[] mRemovedLibraryProjects = null;
1021 /** Whether unknown IClasspathEntry (that were flagged as being added by ADT) are to be
1022 * removed. This is typically only set to <code>true</code> when the project is opened. */
1023 boolean mCleanupCPE = false;
1026 public String toString() {
1027 return String.format(
1028 "LinkUpdateBundle: %1$s (clean: %2$s) > added: %3$s, removed: %4$s, deleted: %5$s", //$NON-NLS-1$
1031 Arrays.toString(mNewLibraryProjects),
1032 Arrays.toString(mRemovedLibraryProjects),
1033 mDeletedLibraryPath);
1037 private final ArrayList<LinkUpdateBundle> mLinkActionBundleQueue =
1038 new ArrayList<LinkUpdateBundle>();
1041 * Queues a {@link LinkUpdateBundle} bundle to be run by a job.
1043 * All action bundles are executed in a job in the exact order they are added.
1044 * This is convenient when several actions must be executed in a job consecutively (instead
1045 * of in parallel as it would happen if each started its own job) but it is impossible
1046 * to manually control the job that's running them (for instance each action is started from
1047 * different callbacks such as {@link IProjectListener#projectOpened(IProject)}.
1049 * If the job is not yet started, or has terminated due to lack of action bundle, it is
1052 * @param bundle the action bundle to execute
1054 private void queueLinkUpdateBundle(LinkUpdateBundle bundle) {
1055 boolean startJob = false;
1056 synchronized (mLinkActionBundleQueue) {
1057 startJob = mLinkActionBundleQueue.size() == 0;
1058 mLinkActionBundleQueue.add(bundle);
1062 Job job = new Job("Android Library Update") { //$NON-NLS-1$
1064 protected IStatus run(IProgressMonitor monitor) {
1065 // loop until there's no bundle to process
1067 // get the bundle, but don't remove until we're done, or a new job could be
1069 LinkUpdateBundle bundle = null;
1070 synchronized (mLinkActionBundleQueue) {
1071 // there is always a bundle at this point, as they are only removed
1072 // at the end of this method, and the job is only started after adding
1074 bundle = mLinkActionBundleQueue.get(0);
1077 // process the bundle.
1079 updateLibraryLinks(bundle, monitor);
1080 } catch (Exception e) {
1081 AdtPlugin.log(e, "Failed to process bundle: %1$s", //$NON-NLS-1$
1086 // force a recompile
1087 bundle.mProject.build(IncrementalProjectBuilder.FULL_BUILD, monitor);
1088 } catch (Exception e) {
1089 // no need to log those.
1092 // remove it from the list.
1093 synchronized (mLinkActionBundleQueue) {
1094 mLinkActionBundleQueue.remove(0);
1096 // no more bundle to process? done.
1097 if (mLinkActionBundleQueue.size() == 0) {
1098 return Status.OK_STATUS;
1104 job.setPriority(Job.BUILD);
1111 * Adds to a list the resolved {@link IProject} dependencies for a given {@link ProjectState}.
1112 * This recursively goes down to indirect dependencies.
1114 * <strong>The list is filled in an order that is not valid for calling <code>aapt</code>
1116 * Use {@link ProjectState#getFullLibraryProjects()} for use with <code>aapt</code>.
1118 * @param projectState the ProjectState of the project from which to add the libraries.
1119 * @param libraries the list of {@link IProject} to fill.
1121 private void fillProjectDependenciesList(ProjectState projectState,
1122 ArrayList<IProject> libraries) {
1123 for (LibraryState libState : projectState.getLibraries()) {
1124 ProjectState libProjectState = libState.getProjectState();
1126 // only care if the LibraryState has a resolved ProjectState
1127 if (libProjectState != null) {
1128 // try not to add duplicate. This can happen if a project depends on 2 different
1129 // libraries that both depend on the same one.
1130 IProject libProject = libProjectState.getProject();
1131 if (libraries.contains(libProject) == false) {
1132 libraries.add(libProject);
1135 // process the libraries of this library too.
1136 fillProjectDependenciesList(libProjectState, libraries);
1142 * Sets up a path variable for a given project.
1143 * The name of the variable is based on the name of the project. However some valid character
1144 * for project names can be invalid for variable paths.
1145 * {@link #getLibraryVariableName(String)} return the name of the variable based on the
1148 * @param libProject the project
1150 * @see IPathVariableManager
1151 * @see #getLibraryVariableName(String)
1153 private void setupLibraryProject(IProject libProject) {
1154 // if needed add a path var for this library
1155 IPathVariableManager pathVarMgr =
1156 ResourcesPlugin.getWorkspace().getPathVariableManager();
1157 IPath libPath = libProject.getLocation();
1159 final String varName = getLibraryVariableName(libProject.getName());
1161 if (libPath.equals(pathVarMgr.getValue(varName)) == false) {
1163 pathVarMgr.setValue(varName, libPath);
1164 } catch (CoreException e) {
1165 AdtPlugin.logAndPrintError(e, "Library Project",
1166 "Unable to set linked path var '%1$s' for library %2$s: %3$s", //$NON-NLS-1$
1167 varName, libPath.toOSString(), e.getMessage());
1174 * Deletes the path variable that was setup for the given project.
1175 * @param project the project
1176 * @see #disposeLibraryProject(String)
1178 private void disposeLibraryProject(IProject project) {
1179 disposeLibraryProject(project.getName());
1183 * Deletes the path variable that was setup for the given project name.
1184 * The name of the variable is based on the name of the project. However some valid character
1185 * for project names can be invalid for variable paths.
1186 * {@link #getLibraryVariableName(String)} return the name of the variable based on the
1188 * @param projectName the name of the project, unmodified.
1190 private void disposeLibraryProject(String projectName) {
1191 IPathVariableManager pathVarMgr =
1192 ResourcesPlugin.getWorkspace().getPathVariableManager();
1194 final String varName = getLibraryVariableName(projectName);
1196 // remove the value by setting the value to null.
1198 pathVarMgr.setValue(varName, null /*path*/);
1199 } catch (CoreException e) {
1200 String message = String.format("Unable to remove linked path var '%1$s'", //$NON-NLS-1$
1202 AdtPlugin.log(e, message);
1207 * Returns a valid path variable name based on the name of a library project.
1208 * @param name the name of the library project.
1210 private String getLibraryVariableName(String name) {
1211 return "_android_" + name.replaceAll("-", "_"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
1215 * Update the library links for a project
1217 * This does the follow:
1218 * - add/remove the library projects to the main projects dynamic reference list. This is used
1219 * by the builders to receive resource change deltas for library projects and figure out what
1220 * needs to be recompiled/recreated.
1221 * - create new {@link IClasspathEntry} of type {@link IClasspathEntry#CPE_SOURCE} for each
1222 * source folder for each new library project.
1223 * - remove the {@link IClasspathEntry} of type {@link IClasspathEntry#CPE_SOURCE} for each
1224 * source folder for each removed library project.
1225 * - If {@link LinkUpdateBundle#mCleanupCPE} is set to true, all CPE created by ADT that cannot
1226 * be resolved are removed. This should only be used when the project is opened.
1228 * <strong>This must not be called directly. Instead the {@link LinkUpdateBundle} must
1229 * be run through a job with {@link #queueLinkUpdateBundle(LinkUpdateBundle)}.</strong>
1231 * @param bundle The {@link LinkUpdateBundle} action bundle that contains all the parameters
1232 * necessary to execute the action.
1233 * @param monitor an {@link IProgressMonitor}.
1234 * @return an {@link IStatus} with the status of the action.
1236 private IStatus updateLibraryLinks(LinkUpdateBundle bundle, IProgressMonitor monitor) {
1237 if (bundle.mProject.isOpen() == false) {
1238 return Status.OK_STATUS;
1241 // add the library to the list of dynamic references. This is necessary to receive
1242 // notifications that the library content changed in the builders.
1243 IProjectDescription projectDescription = bundle.mProject.getDescription();
1244 IProject[] refs = projectDescription.getDynamicReferences();
1246 if (refs.length > 0) {
1247 ArrayList<IProject> list = new ArrayList<IProject>(Arrays.asList(refs));
1249 // remove a previous library if needed (in case of a rename)
1250 if (bundle.mDeletedLibraryPath != null) {
1251 // since project basically have only one segment that matter,
1252 // just check the names
1253 removeFromList(list, bundle.mDeletedLibraryPath.lastSegment());
1256 if (bundle.mRemovedLibraryProjects != null) {
1257 for (IProject removedProject : bundle.mRemovedLibraryProjects) {
1258 removeFromList(list, removedProject.getName());
1262 // add the new ones if they don't exist
1263 if (bundle.mNewLibraryProjects != null) {
1264 for (IProject newProject : bundle.mNewLibraryProjects) {
1265 if (list.contains(newProject) == false) {
1266 list.add(newProject);
1271 // set the changed list
1272 projectDescription.setDynamicReferences(
1273 list.toArray(new IProject[list.size()]));
1275 if (bundle.mNewLibraryProjects != null) {
1276 projectDescription.setDynamicReferences(bundle.mNewLibraryProjects);
1280 // get the current classpath entries for the project to add the new source
1282 IJavaProject javaProject = JavaCore.create(bundle.mProject);
1283 IClasspathEntry[] entries = javaProject.getRawClasspath();
1284 ArrayList<IClasspathEntry> classpathEntries = new ArrayList<IClasspathEntry>(
1285 Arrays.asList(entries));
1287 IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot();
1289 // loop on the classpath entries and look for CPE_SOURCE entries that
1290 // are linked folders, then record them for comparison later as we add the new
1292 ArrayList<IClasspathEntry> cpeToRemove = new ArrayList<IClasspathEntry>();
1293 for (IClasspathEntry classpathEntry : classpathEntries) {
1294 if (classpathEntry.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
1295 IPath path = classpathEntry.getPath();
1296 IResource linkedRes = wsRoot.findMember(path);
1297 if (linkedRes != null && linkedRes.isLinked() &&
1298 CREATOR_ADT.equals(ProjectHelper.loadStringProperty(
1299 linkedRes, PROP_CREATOR))) {
1301 // add always to list if we're doing clean-up
1302 if (bundle.mCleanupCPE) {
1303 cpeToRemove.add(classpathEntry);
1305 String libName = ProjectHelper.loadStringProperty(linkedRes,
1307 if (libName != null && isRemovedLibrary(bundle, libName)) {
1308 cpeToRemove.add(classpathEntry);
1315 // loop on the projects to add.
1316 if (bundle.mNewLibraryProjects != null) {
1317 for (IProject library : bundle.mNewLibraryProjects) {
1318 if (library.isOpen() == false) {
1321 final String libName = library.getName();
1322 final String varName = getLibraryVariableName(libName);
1324 // get the list of source folders for the library.
1325 ArrayList<IPath> sourceFolderPaths = BaseProjectHelper.getSourceClasspaths(
1328 // loop on all the source folder, ignoring FD_GEN and add them
1330 for (IPath sourceFolderPath : sourceFolderPaths) {
1331 IResource sourceFolder = wsRoot.findMember(sourceFolderPath);
1332 if (sourceFolder == null || sourceFolder.isLinked()) {
1336 IPath relativePath = sourceFolder.getProjectRelativePath();
1337 if (SdkConstants.FD_GEN_SOURCES.equals(relativePath.toString())) {
1341 // create the linked path
1342 IPath linkedPath = new Path(varName).append(relativePath);
1344 // look for an existing CPE that has the same linked path and that was
1345 // going to be removed.
1346 IClasspathEntry match = findClasspathEntryMatch(cpeToRemove, linkedPath,
1349 if (match == null) {
1350 // no match, create one
1351 // get a string version, to make up the linked folder name
1352 String srcFolderName = relativePath.toString().replace(
1357 String folderName = libName + "_" + srcFolderName; //$NON-NLS-1$
1359 // create a linked resource for the library using the path var.
1360 IFolder libSrc = bundle.mProject.getFolder(folderName);
1361 IPath libSrcPath = libSrc.getFullPath();
1363 // check if there's a CPE that would conflict, in which case it needs to
1364 // be removed (this can happen for existing CPE that don't match an open
1366 match = findClasspathEntryMatch(classpathEntries, null/*rawPath*/,
1368 if (match != null) {
1369 classpathEntries.remove(match);
1372 // the path of the linked resource is based on the path variable
1373 // representing the library project, followed by the source folder name.
1374 libSrc.createLink(linkedPath, IResource.REPLACE, monitor);
1376 // mark it as derived so that Team plug-in ignore this
1377 libSrc.setDerived(true);
1379 // set some persistent properties on it to know that it was
1381 ProjectHelper.saveStringProperty(libSrc, PROP_CREATOR, CREATOR_ADT);
1382 ProjectHelper.saveResourceProperty(libSrc, PROP_LIBRARY, library);
1383 ProjectHelper.saveStringProperty(libSrc, PROP_LIBRARY_NAME,
1386 // add the source folder to the classpath entries
1387 classpathEntries.add(JavaCore.newSourceEntry(libSrcPath));
1389 // there's a valid match, do nothing, but remove the match from
1390 // the list of previously existing CPE.
1391 cpeToRemove.remove(match);
1397 // remove the CPE that should be removed.
1398 classpathEntries.removeAll(cpeToRemove);
1401 javaProject.setRawClasspath(
1402 classpathEntries.toArray(new IClasspathEntry[classpathEntries.size()]),
1405 // and delete the folders of the CPE that were removed (must be done after)
1406 for (IClasspathEntry cpe : cpeToRemove) {
1407 IResource res = wsRoot.findMember(cpe.getPath());
1408 res.delete(true, monitor);
1411 return Status.OK_STATUS;
1412 } catch (CoreException e) {
1413 AdtPlugin.logAndPrintError(e, bundle.mProject.getName(),
1414 "Failed to create library links: %1$s", //$NON-NLS-1$
1416 return e.getStatus();
1420 private boolean isRemovedLibrary(LinkUpdateBundle bundle, String libName) {
1421 if (bundle.mDeletedLibraryPath != null &&
1422 libName.equals(bundle.mDeletedLibraryPath.lastSegment())) {
1426 if (bundle.mRemovedLibraryProjects != null) {
1427 for (IProject removedProject : bundle.mRemovedLibraryProjects) {
1428 if (libName.equals(removedProject.getName())) {
1438 * Computes the library difference based on a previous list and a current state, and creates
1439 * a {@link LinkUpdateBundle} action to update the given project.
1440 * @param project The current project state
1441 * @param oldLibraries the list of old libraries. Typically the result of
1442 * {@link ProjectState#getFullLibraryProjects()} before the ProjectState is updated.
1443 * @return null if there no action to take, or a {@link LinkUpdateBundle} object to run.
1445 private LinkUpdateBundle getLinkBundle(ProjectState project, IProject[] oldLibraries) {
1446 // get the new full list of projects
1447 IProject[] newLibraries = project.getFullLibraryProjects();
1449 // and build the real difference. A list of new projects and a list of
1451 // This is not the same as the added/removed libraries because libraries
1452 // could be indirect dependencies through several different direct
1453 // dependencies so it's easier to compare the full lists before and after
1456 List<IProject> addedLibs = new ArrayList<IProject>();
1457 List<IProject> removedLibs = new ArrayList<IProject>();
1459 // first get the list of new projects.
1460 for (IProject newLibrary : newLibraries) {
1461 boolean found = false;
1462 for (IProject oldLibrary : oldLibraries) {
1463 if (newLibrary.equals(oldLibrary)) {
1469 // if it was not found in the old libraries, it's really new
1470 if (found == false) {
1471 addedLibs.add(newLibrary);
1475 // now the list of removed projects.
1476 for (IProject oldLibrary : oldLibraries) {
1477 boolean found = false;
1478 for (IProject newLibrary : newLibraries) {
1479 if (newLibrary.equals(oldLibrary)) {
1485 // if it was not found in the new libraries, it's really been removed
1486 if (found == false) {
1487 removedLibs.add(oldLibrary);
1491 if (addedLibs.size() > 0 || removedLibs.size() > 0) {
1492 LinkUpdateBundle bundle = new LinkUpdateBundle();
1493 bundle.mProject = project.getProject();
1494 bundle.mNewLibraryProjects =
1495 addedLibs.toArray(new IProject[addedLibs.size()]);
1496 bundle.mRemovedLibraryProjects =
1497 removedLibs.toArray(new IProject[removedLibs.size()]);
1505 * Removes a project from a list based on its name.
1506 * @param projects the list of projects.
1507 * @param name the name of the project to remove.
1509 private void removeFromList(List<IProject> projects, String name) {
1510 final int count = projects.size();
1511 for (int i = 0 ; i < count ; i++) {
1512 // since project basically have only one segment that matter,
1513 // just check the names
1514 if (projects.get(i).getName().equals(name)) {
1522 * Returns a {@link IClasspathEntry} from the given list whose linked path match the given path.
1523 * @param cpeList a list of {@link IClasspathEntry} of {@link IClasspathEntry#getEntryKind()}
1524 * {@link IClasspathEntry#CPE_SOURCE} whose {@link IClasspathEntry#getPath()}
1525 * points to a linked folder.
1526 * @param rawPath the raw path to compare to. Can be null if <var>path</var> is used instead.
1527 * @param path the path to compare to. Can be null if <var>rawPath</var> is used instead.
1528 * @return the matching IClasspathEntry or null.
1530 private IClasspathEntry findClasspathEntryMatch(ArrayList<IClasspathEntry> cpeList,
1531 IPath rawPath, IPath path) {
1532 IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot();
1533 for (IClasspathEntry cpe : cpeList) {
1534 IPath cpePath = cpe.getPath();
1535 // test the normal path of the resource.
1536 if (path != null && path.equals(cpePath)) {
1540 IResource res = wsRoot.findMember(cpePath);
1541 // getRawLocation returns the path that the linked folder points to.
1542 if (rawPath != null && res.getRawLocation().equals(rawPath)) {
1551 * Updates all existing projects with a given list of new/updated libraries.
1552 * This loops through all opened projects and check if they depend on any of the given
1553 * library project, and if they do, they are linked together.
1554 * @param libraries the list of new/updated library projects.
1556 private void updateProjectsWithNewLibraries(List<ProjectState> libraries) {
1557 if (libraries.size() == 0) {
1561 ArrayList<ProjectState> updatedLibraries = new ArrayList<ProjectState>();
1562 synchronized (sLock) {
1563 // for each projects, look for projects that depend on it, and update them.
1564 // Once they are updated (meaning ProjectState#needs() has been called on them),
1565 // we add them to the list so that can be updated as well.
1566 for (ProjectState projectState : sProjectStateMap.values()) {
1567 // record the current library dependencies
1568 IProject[] oldLibraries = projectState.getFullLibraryProjects();
1570 boolean needLibraryDependenciesUpdated = false;
1571 for (ProjectState library : libraries) {
1572 // Normally we would only need to test if ProjectState#needs returns non null,
1573 // meaning the link between the project and the library has not been
1575 // However what matters here is that the library is a dependency,
1576 // period. If the library project was updated, then we redo the link,
1577 // with all indirect dependencies (which *have* changed, since this is
1578 // what this method is all about.)
1579 // We still need to call ProjectState#needs to make the link in case it's not
1580 // been done yet (which can happen if the library project was just opened).
1581 if (projectState != library) {
1582 // call needs in case this new library was just opened, and the link needs
1584 LibraryState libState = projectState.needs(library);
1585 if (libState == null && projectState.dependsOn(library)) {
1586 // ProjectState.needs only returns true if the library was needed.
1587 // but we also need to check the case where the project depends on
1588 // the library but the link was already done.
1589 needLibraryDependenciesUpdated = true;
1594 if (needLibraryDependenciesUpdated) {
1595 projectState.updateFullLibraryList();
1598 LinkUpdateBundle bundle = getLinkBundle(projectState, oldLibraries);
1599 if (bundle != null) {
1600 queueLinkUpdateBundle(bundle);
1602 // if this updated project is a library, add it to the list, so that
1603 // projects depending on it get updated too.
1604 if (projectState.isLibrary() &&
1605 updatedLibraries.contains(projectState) == false) {
1606 updatedLibraries.add(projectState);
1612 // done, but there may be updated projects that were libraries, so we need to do the same
1613 // for this libraries, to update the project there were depending on.
1614 updateProjectsWithNewLibraries(updatedLibraries);
1618 * Computes a new IPath targeting a given target, but relative to a given base.
1619 * <p/>{@link IPath#makeRelativeTo(IPath, IPath)} is only available in 3.5 and later.
1620 * <p/>This is based on the implementation {@link Path#makeRelativeTo(IPath)}.
1621 * @param target the target of the IPath
1622 * @param base the IPath to base the relative path on.
1623 * @return the relative IPath
1625 public static IPath makeRelativeTo(IPath target, IPath base) {
1626 //can't make relative if devices are not equal
1627 if (target.getDevice() != base.getDevice() && (target.getDevice() == null ||
1628 !target.getDevice().equalsIgnoreCase(base.getDevice())))
1630 int commonLength = target.matchingFirstSegments(base);
1631 final int differenceLength = base.segmentCount() - commonLength;
1632 final int newSegmentLength = differenceLength + target.segmentCount() - commonLength;
1633 if (newSegmentLength == 0)
1635 String[] newSegments = new String[newSegmentLength];
1636 //add parent references for each segment different from the base
1637 Arrays.fill(newSegments, 0, differenceLength, ".."); //$NON-NLS-1$
1638 //append the segments of this path not in common with the base
1639 System.arraycopy(target.segments(), commonLength, newSegments,
1640 differenceLength, newSegmentLength - differenceLength);
1642 StringBuilder sb = new StringBuilder();
1643 for (String s : newSegments) {
1644 sb.append(s).append('/');
1647 return new Path(null, sb.toString());