| /* |
| * 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(); |
| } |
| } |