diff options
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.java | 740 |
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(); + } +} |