aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/ProjectState.java
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/ProjectState.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/ProjectState.java740
1 files changed, 740 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/ProjectState.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/ProjectState.java
new file mode 100644
index 000000000..74c985784
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/sdk/ProjectState.java
@@ -0,0 +1,740 @@
+/*
+ * Copyright (C) 2010 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.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.sdklib.BuildToolInfo;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.internal.project.ProjectProperties;
+import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
+
+/**
+ * Centralized state for Android Eclipse project.
+ * <p>This gives raw access to the properties (from <code>project.properties</code>), as well
+ * as direct access to target and library information.
+ *
+ * This also gives access to library information.
+ *
+ * {@link #isLibrary()} indicates if the project is a library.
+ * {@link #hasLibraries()} and {@link #getLibraries()} give access to the libraries through
+ * instances of {@link LibraryState}. A {@link LibraryState} instance is a link between a main
+ * project and its library. Theses instances are owned by the {@link ProjectState}.
+ *
+ * {@link #isMissingLibraries()} will indicate if the project has libraries that are not resolved.
+ * Unresolved libraries are libraries that do not have any matching opened Eclipse project.
+ * When there are missing libraries, the {@link LibraryState} instance for them will return null
+ * for {@link LibraryState#getProjectState()}.
+ *
+ */
+public final class ProjectState {
+
+ /**
+ * A class that represents a library linked to a project.
+ * <p/>It does not represent the library uniquely. Instead the {@link LibraryState} is linked
+ * to the main project which is accessible through {@link #getMainProjectState()}.
+ * <p/>If a library is used by two different projects, then there will be two different
+ * instances of {@link LibraryState} for the library.
+ *
+ * @see ProjectState#getLibrary(IProject)
+ */
+ public final class LibraryState {
+ private String mRelativePath;
+ private ProjectState mProjectState;
+ private String mPath;
+
+ private LibraryState(String relativePath) {
+ mRelativePath = relativePath;
+ }
+
+ /**
+ * Returns the {@link ProjectState} of the main project using this library.
+ */
+ public ProjectState getMainProjectState() {
+ return ProjectState.this;
+ }
+
+ /**
+ * Closes the library. This resets the IProject from this object ({@link #getProjectState()} will
+ * return <code>null</code>), and updates the main project data so that the library
+ * {@link IProject} object does not show up in the return value of
+ * {@link ProjectState#getFullLibraryProjects()}.
+ */
+ public void close() {
+ mProjectState.removeParentProject(getMainProjectState());
+ mProjectState = null;
+ mPath = null;
+
+ getMainProjectState().updateFullLibraryList();
+ }
+
+ private void setRelativePath(String relativePath) {
+ mRelativePath = relativePath;
+ }
+
+ private void setProject(ProjectState project) {
+ mProjectState = project;
+ mPath = project.getProject().getLocation().toOSString();
+ mProjectState.addParentProject(getMainProjectState());
+
+ getMainProjectState().updateFullLibraryList();
+ }
+
+ /**
+ * Returns the relative path of the library from the main project.
+ * <p/>This is identical to the value defined in the main project's project.properties.
+ */
+ public String getRelativePath() {
+ return mRelativePath;
+ }
+
+ /**
+ * Returns the {@link ProjectState} item for the library. This can be null if the project
+ * is not actually opened in Eclipse.
+ */
+ public ProjectState getProjectState() {
+ return mProjectState;
+ }
+
+ /**
+ * Returns the OS-String location of the library project.
+ * <p/>This is based on location of the Eclipse project that matched
+ * {@link #getRelativePath()}.
+ *
+ * @return The project location, or null if the project is not opened in Eclipse.
+ */
+ public String getProjectLocation() {
+ return mPath;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof LibraryState) {
+ // the only thing that's always non-null is the relative path.
+ LibraryState objState = (LibraryState)obj;
+ return mRelativePath.equals(objState.mRelativePath) &&
+ getMainProjectState().equals(objState.getMainProjectState());
+ } else if (obj instanceof ProjectState || obj instanceof IProject) {
+ return mProjectState != null && mProjectState.equals(obj);
+ } else if (obj instanceof String) {
+ return normalizePath(mRelativePath).equals(normalizePath((String) obj));
+ }
+
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return normalizePath(mRelativePath).hashCode();
+ }
+ }
+
+ private final IProject mProject;
+ private final ProjectProperties mProperties;
+ private IAndroidTarget mTarget;
+ private BuildToolInfo mBuildToolInfo;
+
+ /**
+ * list of libraries. Access to this list must be protected by
+ * <code>synchronized(mLibraries)</code>, but it is important that such code do not call
+ * out to other classes (especially those protected by {@link Sdk#getLock()}.)
+ */
+ private final ArrayList<LibraryState> mLibraries = new ArrayList<LibraryState>();
+ /** Cached list of all IProject instances representing the resolved libraries, including
+ * indirect dependencies. This must never be null. */
+ private List<IProject> mLibraryProjects = Collections.emptyList();
+ /**
+ * List of parent projects. When this instance is a library ({@link #isLibrary()} returns
+ * <code>true</code>) then this is filled with projects that depends on this project.
+ */
+ private final ArrayList<ProjectState> mParentProjects = new ArrayList<ProjectState>();
+
+ ProjectState(IProject project, ProjectProperties properties) {
+ if (project == null || properties == null) {
+ throw new NullPointerException();
+ }
+
+ mProject = project;
+ mProperties = properties;
+
+ // load the libraries
+ synchronized (mLibraries) {
+ int index = 1;
+ while (true) {
+ String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index++);
+ String rootPath = mProperties.getProperty(propName);
+
+ if (rootPath == null) {
+ break;
+ }
+
+ mLibraries.add(new LibraryState(convertPath(rootPath)));
+ }
+ }
+ }
+
+ public IProject getProject() {
+ return mProject;
+ }
+
+ public ProjectProperties getProperties() {
+ return mProperties;
+ }
+
+ public @Nullable String getProperty(@NonNull String name) {
+ if (mProperties != null) {
+ return mProperties.getProperty(name);
+ }
+
+ return null;
+ }
+
+ public void setTarget(IAndroidTarget target) {
+ mTarget = target;
+ }
+
+ /**
+ * Returns the project's target's hash string.
+ * <p/>If {@link #getTarget()} returns a valid object, then this returns the value of
+ * {@link IAndroidTarget#hashString()}.
+ * <p/>Otherwise this will return the value of the property
+ * {@link ProjectProperties#PROPERTY_TARGET} from {@link #getProperties()} (if valid).
+ * @return the target hash string or null if not found.
+ */
+ public String getTargetHashString() {
+ if (mTarget != null) {
+ return mTarget.hashString();
+ }
+
+ return mProperties.getProperty(ProjectProperties.PROPERTY_TARGET);
+ }
+
+ public IAndroidTarget getTarget() {
+ return mTarget;
+ }
+
+ public void setBuildToolInfo(BuildToolInfo buildToolInfo) {
+ mBuildToolInfo = buildToolInfo;
+ }
+
+ public BuildToolInfo getBuildToolInfo() {
+ return mBuildToolInfo;
+ }
+
+ /**
+ * Returns the build tools version from the project's properties.
+ * @return the value or null
+ */
+ @Nullable
+ public String getBuildToolInfoVersion() {
+ return mProperties.getProperty(ProjectProperties.PROPERTY_BUILD_TOOLS);
+ }
+
+ public boolean getRenderScriptSupportMode() {
+ String supportModeValue = mProperties.getProperty(ProjectProperties.PROPERTY_RS_SUPPORT);
+ if (supportModeValue != null) {
+ return Boolean.parseBoolean(supportModeValue);
+ }
+
+ return false;
+ }
+
+ public static class LibraryDifference {
+ public boolean removed = false;
+ public boolean added = false;
+
+ public boolean hasDiff() {
+ return removed || added;
+ }
+ }
+
+ /**
+ * Reloads the content of the properties.
+ * <p/>This also reset the reference to the target as it may have changed, therefore this
+ * should be followed by a call to {@link Sdk#loadTarget(ProjectState)}.
+ *
+ * <p/>If the project libraries changes, they are updated to a certain extent.<br>
+ * Removed libraries are removed from the state list, and added to the {@link LibraryDifference}
+ * object that is returned so that they can be processed.<br>
+ * Added libraries are added to the state (as new {@link LibraryState} objects), but their
+ * IProject is not resolved. {@link ProjectState#needs(ProjectState)} should be called
+ * afterwards to properly initialize the libraries.
+ *
+ * @return an instance of {@link LibraryDifference} describing the change in libraries.
+ */
+ public LibraryDifference reloadProperties() {
+ mTarget = null;
+ mProperties.reload();
+
+ // compare/reload the libraries.
+
+ // if the order change it won't impact the java part, so instead try to detect removed/added
+ // libraries.
+
+ LibraryDifference diff = new LibraryDifference();
+
+ synchronized (mLibraries) {
+ List<LibraryState> oldLibraries = new ArrayList<LibraryState>(mLibraries);
+ mLibraries.clear();
+
+ // load the libraries
+ int index = 1;
+ while (true) {
+ String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index++);
+ String rootPath = mProperties.getProperty(propName);
+
+ if (rootPath == null) {
+ break;
+ }
+
+ // search for a library with the same path (not exact same string, but going
+ // to the same folder).
+ String convertedPath = convertPath(rootPath);
+ boolean found = false;
+ for (int i = 0 ; i < oldLibraries.size(); i++) {
+ LibraryState libState = oldLibraries.get(i);
+ if (libState.equals(convertedPath)) {
+ // it's a match. move it back to mLibraries and remove it from the
+ // old library list.
+ found = true;
+ mLibraries.add(libState);
+ oldLibraries.remove(i);
+ break;
+ }
+ }
+
+ if (found == false) {
+ diff.added = true;
+ mLibraries.add(new LibraryState(convertedPath));
+ }
+ }
+
+ // whatever's left in oldLibraries is removed.
+ diff.removed = oldLibraries.size() > 0;
+
+ // update the library with what IProjet are known at the time.
+ updateFullLibraryList();
+ }
+
+ return diff;
+ }
+
+ /**
+ * Returns the list of {@link LibraryState}.
+ */
+ public List<LibraryState> getLibraries() {
+ synchronized (mLibraries) {
+ return Collections.unmodifiableList(mLibraries);
+ }
+ }
+
+ /**
+ * Returns all the <strong>resolved</strong> library projects, including indirect dependencies.
+ * The list is ordered to match the library priority order for resource processing with
+ * <code>aapt</code>.
+ * <p/>If some dependencies are not resolved (or their projects is not opened in Eclipse),
+ * they will not show up in this list.
+ * @return the resolved projects as an unmodifiable list. May be an empty.
+ */
+ public List<IProject> getFullLibraryProjects() {
+ return mLibraryProjects;
+ }
+
+ /**
+ * Returns whether this is a library project.
+ */
+ public boolean isLibrary() {
+ String value = mProperties.getProperty(ProjectProperties.PROPERTY_LIBRARY);
+ return value != null && Boolean.valueOf(value);
+ }
+
+ /**
+ * Returns whether the project depends on one or more libraries.
+ */
+ public boolean hasLibraries() {
+ synchronized (mLibraries) {
+ return mLibraries.size() > 0;
+ }
+ }
+
+ /**
+ * Returns whether the project is missing some required libraries.
+ */
+ public boolean isMissingLibraries() {
+ synchronized (mLibraries) {
+ for (LibraryState state : mLibraries) {
+ if (state.getProjectState() == null) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the {@link LibraryState} object for a given {@link IProject}.
+ * </p>This can only return a non-null object if the link between the main project's
+ * {@link IProject} and the library's {@link IProject} was done.
+ *
+ * @return the matching LibraryState or <code>null</code>
+ *
+ * @see #needs(ProjectState)
+ */
+ public LibraryState getLibrary(IProject library) {
+ synchronized (mLibraries) {
+ for (LibraryState state : mLibraries) {
+ ProjectState ps = state.getProjectState();
+ if (ps != null && ps.getProject().equals(library)) {
+ return state;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the {@link LibraryState} object for a given <var>name</var>.
+ * </p>This can only return a non-null object if the link between the main project's
+ * {@link IProject} and the library's {@link IProject} was done.
+ *
+ * @return the matching LibraryState or <code>null</code>
+ *
+ * @see #needs(IProject)
+ */
+ public LibraryState getLibrary(String name) {
+ synchronized (mLibraries) {
+ for (LibraryState state : mLibraries) {
+ ProjectState ps = state.getProjectState();
+ if (ps != null && ps.getProject().getName().equals(name)) {
+ return state;
+ }
+ }
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Returns whether a given library project is needed by the receiver.
+ * <p/>If the library is needed, this finds the matching {@link LibraryState}, initializes it
+ * so that it contains the library's {@link IProject} object (so that
+ * {@link LibraryState#getProjectState()} does not return null) and then returns it.
+ *
+ * @param libraryProject the library project to check.
+ * @return a non null object if the project is a library dependency,
+ * <code>null</code> otherwise.
+ *
+ * @see LibraryState#getProjectState()
+ */
+ public LibraryState needs(ProjectState libraryProject) {
+ // compute current location
+ File projectFile = mProject.getLocation().toFile();
+
+ // get the location of the library.
+ File libraryFile = libraryProject.getProject().getLocation().toFile();
+
+ // loop on all libraries and check if the path match
+ synchronized (mLibraries) {
+ for (LibraryState state : mLibraries) {
+ if (state.getProjectState() == null) {
+ File library = new File(projectFile, state.getRelativePath());
+ try {
+ File absPath = library.getCanonicalFile();
+ if (absPath.equals(libraryFile)) {
+ state.setProject(libraryProject);
+ return state;
+ }
+ } catch (IOException e) {
+ // ignore this library
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns whether the project depends on a given <var>library</var>
+ * @param library the library to check.
+ * @return true if the project depends on the library. This is not affected by whether the link
+ * was done through {@link #needs(ProjectState)}.
+ */
+ public boolean dependsOn(ProjectState library) {
+ synchronized (mLibraries) {
+ for (LibraryState state : mLibraries) {
+ if (state != null && state.getProjectState() != null &&
+ library.getProject().equals(state.getProjectState().getProject())) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+
+ /**
+ * Updates a library with a new path.
+ * <p/>This method acts both as a check and an action. If the project does not depend on the
+ * given <var>oldRelativePath</var> then no action is done and <code>null</code> is returned.
+ * <p/>If the project depends on the library, then the project is updated with the new path,
+ * and the {@link LibraryState} for the library is returned.
+ * <p/>Updating the project does two things:<ul>
+ * <li>Update LibraryState with new relative path and new {@link IProject} object.</li>
+ * <li>Update the main project's <code>project.properties</code> with the new relative path
+ * for the changed library.</li>
+ * </ul>
+ *
+ * @param oldRelativePath the old library path relative to this project
+ * @param newRelativePath the new library path relative to this project
+ * @param newLibraryState the new {@link ProjectState} object.
+ * @return a non null object if the project depends on the library.
+ *
+ * @see LibraryState#getProjectState()
+ */
+ public LibraryState updateLibrary(String oldRelativePath, String newRelativePath,
+ ProjectState newLibraryState) {
+ // compute current location
+ File projectFile = mProject.getLocation().toFile();
+
+ // loop on all libraries and check if the path matches
+ synchronized (mLibraries) {
+ for (LibraryState state : mLibraries) {
+ if (state.getProjectState() == null) {
+ try {
+ // oldRelativePath may not be the same exact string as the
+ // one in the project properties (trailing separator could be different
+ // for instance).
+ // Use java.io.File to deal with this and also do a platform-dependent
+ // path comparison
+ File library1 = new File(projectFile, oldRelativePath);
+ File library2 = new File(projectFile, state.getRelativePath());
+ if (library1.getCanonicalPath().equals(library2.getCanonicalPath())) {
+ // save the exact property string to replace.
+ String oldProperty = state.getRelativePath();
+
+ // then update the LibraryPath.
+ state.setRelativePath(newRelativePath);
+ state.setProject(newLibraryState);
+
+ // update the project.properties file
+ IStatus status = replaceLibraryProperty(oldProperty, newRelativePath);
+ if (status != null) {
+ if (status.getSeverity() != IStatus.OK) {
+ // log the error somehow.
+ }
+ } else {
+ // This should not happen since the library wouldn't be here in the
+ // first place
+ }
+
+ // return the LibraryState object.
+ return state;
+ }
+ } catch (IOException e) {
+ // ignore this library
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+
+ private void addParentProject(ProjectState parentState) {
+ mParentProjects.add(parentState);
+ }
+
+ private void removeParentProject(ProjectState parentState) {
+ mParentProjects.remove(parentState);
+ }
+
+ public List<ProjectState> getParentProjects() {
+ return Collections.unmodifiableList(mParentProjects);
+ }
+
+ /**
+ * Computes the transitive closure of projects referencing this project as a
+ * library project
+ *
+ * @return a collection (in any order) of project states for projects that
+ * directly or indirectly include this project state's project as a
+ * library project
+ */
+ public Collection<ProjectState> getFullParentProjects() {
+ Set<ProjectState> result = new HashSet<ProjectState>();
+ addParentProjects(result, this);
+ return result;
+ }
+
+ /** Adds all parent projects of the given project, transitively, into the given parent set */
+ private static void addParentProjects(Set<ProjectState> parents, ProjectState state) {
+ for (ProjectState s : state.mParentProjects) {
+ if (!parents.contains(s)) {
+ parents.add(s);
+ addParentProjects(parents, s);
+ }
+ }
+ }
+
+ /**
+ * Update the value of a library dependency.
+ * <p/>This loops on all current dependency looking for the value to replace and then replaces
+ * it.
+ * <p/>This both updates the in-memory {@link #mProperties} values and on-disk
+ * project.properties file.
+ * @param oldValue the old value to replace
+ * @param newValue the new value to set.
+ * @return the status of the replacement. If null, no replacement was done (value not found).
+ */
+ private IStatus replaceLibraryProperty(String oldValue, String newValue) {
+ int index = 1;
+ while (true) {
+ String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index++);
+ String rootPath = mProperties.getProperty(propName);
+
+ if (rootPath == null) {
+ break;
+ }
+
+ if (rootPath.equals(oldValue)) {
+ // need to update the properties. Get a working copy to change it and save it on
+ // disk since ProjectProperties is read-only.
+ ProjectPropertiesWorkingCopy workingCopy = mProperties.makeWorkingCopy();
+ workingCopy.setProperty(propName, newValue);
+ try {
+ workingCopy.save();
+
+ // reload the properties with the new values from the disk.
+ mProperties.reload();
+ } catch (Exception e) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, String.format(
+ "Failed to save %1$s for project %2$s",
+ mProperties.getType() .getFilename(), mProject.getName()),
+ e);
+
+ }
+ return Status.OK_STATUS;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Update the full library list, including indirect dependencies. The result is returned by
+ * {@link #getFullLibraryProjects()}.
+ */
+ void updateFullLibraryList() {
+ ArrayList<IProject> list = new ArrayList<IProject>();
+ synchronized (mLibraries) {
+ buildFullLibraryDependencies(mLibraries, list);
+ }
+
+ mLibraryProjects = Collections.unmodifiableList(list);
+ }
+
+ /**
+ * Resolves a given list of libraries, finds out if they depend on other libraries, and
+ * returns a full list of all the direct and indirect dependencies in the proper order (first
+ * is higher priority when calling aapt).
+ * @param inLibraries the libraries to resolve
+ * @param outLibraries where to store all the libraries.
+ */
+ private void buildFullLibraryDependencies(List<LibraryState> inLibraries,
+ ArrayList<IProject> outLibraries) {
+ // loop in the inverse order to resolve dependencies on the libraries, so that if a library
+ // is required by two higher level libraries it can be inserted in the correct place
+ for (int i = inLibraries.size() - 1 ; i >= 0 ; i--) {
+ LibraryState library = inLibraries.get(i);
+
+ // get its libraries if possible
+ ProjectState libProjectState = library.getProjectState();
+ if (libProjectState != null) {
+ List<LibraryState> dependencies = libProjectState.getLibraries();
+
+ // build the dependencies for those libraries
+ buildFullLibraryDependencies(dependencies, outLibraries);
+
+ // and add the current library (if needed) in front (higher priority)
+ if (outLibraries.contains(libProjectState.getProject()) == false) {
+ outLibraries.add(0, libProjectState.getProject());
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Converts a path containing only / by the proper platform separator.
+ */
+ private String convertPath(String path) {
+ return path.replaceAll("/", Matcher.quoteReplacement(File.separator)); //$NON-NLS-1$
+ }
+
+ /**
+ * Normalizes a relative path.
+ */
+ private String normalizePath(String path) {
+ path = convertPath(path);
+ if (path.endsWith("/")) { //$NON-NLS-1$
+ path = path.substring(0, path.length() - 1);
+ }
+ return path;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof ProjectState) {
+ return mProject.equals(((ProjectState) obj).mProject);
+ } else if (obj instanceof IProject) {
+ return mProject.equals(obj);
+ }
+
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return mProject.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return mProject.getName();
+ }
+}