2 * Copyright (C) 2010 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.project;
19 import com.android.ide.eclipse.adt.AdtPlugin;
20 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
21 import com.android.sdklib.IAndroidTarget;
22 import com.android.sdklib.internal.project.ApkConfigurationHelper;
23 import com.android.sdklib.internal.project.ApkSettings;
24 import com.android.sdklib.internal.project.ProjectProperties;
26 import org.eclipse.core.resources.IProject;
27 import org.eclipse.core.runtime.IStatus;
28 import org.eclipse.core.runtime.Status;
31 import java.io.IOException;
32 import java.util.ArrayList;
33 import java.util.List;
36 * Centralized state for Android Eclipse project.
37 * <p>This gives raw access to the properties (from <code>default.properties</code>), as well
38 * as direct access to target, apksettings and library information.
41 public final class ProjectState {
44 * A class that represents a library linked to a project.
45 * <p/>It does not represent the library uniquely. Instead the {@link LibraryState} is linked
46 * to the main project which is accessible through {@link #getMainProjectState()}.
47 * <p/>If a library is used by two different projects, then there will be two different
48 * instances of {@link LibraryState} for the library.
50 * @see ProjectState#getLibrary(IProject)
52 public final class LibraryState {
53 private String mRelativePath;
54 private ProjectState mProjectState;
57 private LibraryState(String relativePath) {
58 mRelativePath = relativePath;
62 * Returns the {@link ProjectState} of the main project using this library.
64 public ProjectState getMainProjectState() {
65 return ProjectState.this;
69 * Closes the library. This resets the IProject from this object ({@link #getProjectState()} will
70 * return <code>null</code>), and updates the main project data so that the library
71 * {@link IProject} object does not show up in the return value of
72 * {@link ProjectState#getLibraryProjects()}.
81 private void setRelativePath(String relativePath) {
82 mRelativePath = relativePath;
85 private void setProject(ProjectState project) {
86 mProjectState = project;
87 mPath = project.getProject().getLocation().toOSString();
93 * Returns the relative path of the library from the main project.
94 * <p/>This is identical to the value defined in the main project's default.properties.
96 public String getRelativePath() {
101 * Returns the {@link ProjectState} item for the library. This can be null if the project
102 * is not actually opened in Eclipse.
104 public ProjectState getProjectState() {
105 return mProjectState;
109 * Returns the OS-String location of the library project.
110 * <p/>This is based on location of the Eclipse project that matched
111 * {@link #getRelativePath()}.
113 * @return The project location, or null if the project is not opened in Eclipse.
115 public String getProjectLocation() {
120 public boolean equals(Object obj) {
121 if (obj instanceof LibraryState) {
122 // the only thing that's always non-null is the relative path.
123 LibraryState objState = (LibraryState)obj;
124 return mRelativePath.equals(objState.mRelativePath) &&
125 getMainProjectState().equals(objState.getMainProjectState());
126 } else if (obj instanceof ProjectState || obj instanceof IProject) {
127 return mProjectState != null && mProjectState.equals(obj);
128 } else if (obj instanceof String) {
129 return mRelativePath.equals(obj);
136 public int hashCode() {
137 return mRelativePath.hashCode();
141 private final IProject mProject;
142 private final ProjectProperties mProperties;
144 * list of libraries. Access to this list must be protected by
145 * <code>synchronized(mLibraries)</code>, but it is important that such code do not call
146 * out to other classes (especially those protected by {@link Sdk#getLock()}.)
148 private final ArrayList<LibraryState> mLibraries = new ArrayList<LibraryState>();
149 private IAndroidTarget mTarget;
150 private ApkSettings mApkSettings;
151 private IProject[] mLibraryProjects;
153 public ProjectState(IProject project, ProjectProperties properties) {
155 mProperties = properties;
157 // load the ApkSettings
158 mApkSettings = ApkConfigurationHelper.getSettings(properties);
160 // load the libraries
161 synchronized (mLibraries) {
164 String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index++);
165 String rootPath = mProperties.getProperty(propName);
167 if (rootPath == null) {
171 mLibraries.add(new LibraryState(convertPath(rootPath)));
176 public IProject getProject() {
180 public ProjectProperties getProperties() {
184 public void setTarget(IAndroidTarget target) {
189 * Returns the project's target's hash string.
190 * <p/>If {@link #getTarget()} returns a valid object, then this returns the value of
191 * {@link IAndroidTarget#hashString()}.
192 * <p/>Otherwise this will return the value of the property
193 * {@link ProjectProperties#PROPERTY_TARGET} from {@link #getProperties()} (if valid).
194 * @return the target hash string or null if not found.
196 public String getTargetHashString() {
197 if (mTarget != null) {
198 return mTarget.hashString();
201 if (mProperties != null) {
202 return mProperties.getProperty(ProjectProperties.PROPERTY_TARGET);
208 public IAndroidTarget getTarget() {
212 public static class LibraryDifference {
213 public List<LibraryState> removed = new ArrayList<LibraryState>();
214 public boolean added = false;
216 public boolean hasDiff() {
217 return removed.size() > 0 || added;
222 * Reloads the content of the properties.
223 * <p/>This also reset the reference to the target as it may have changed.
224 * <p/>This should be followed by a call to {@link Sdk#loadTarget(ProjectState)}.
226 * @return an instance of {@link LibraryDifference} describing the change in libraries.
228 public LibraryDifference reloadProperties() {
230 mProperties.reload();
232 // compare/reload the libraries.
234 // if the order change it won't impact the java part, so instead try to detect removed/added
237 LibraryDifference diff = new LibraryDifference();
239 synchronized (mLibraries) {
240 List<LibraryState> oldLibraries = new ArrayList<LibraryState>(mLibraries);
243 // load the libraries
246 String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index++);
247 String rootPath = mProperties.getProperty(propName);
249 if (rootPath == null) {
253 // search for a library with the same path (not exact same string, but going
254 // to the same folder).
255 String convertedPath = convertPath(rootPath);
256 boolean found = false;
257 for (int i = 0 ; i < oldLibraries.size(); i++) {
258 LibraryState libState = oldLibraries.get(i);
259 if (libState.equals(convertedPath)) {
260 // it's a match. move it back to mLibraries and remove it from the
263 mLibraries.add(libState);
264 oldLibraries.remove(i);
269 if (found == false) {
271 mLibraries.add(new LibraryState(convertedPath));
275 // whatever's left in oldLibraries is removed.
276 diff.removed.addAll(oldLibraries);
278 // update the library with what IProjet are known at the time.
285 public void setApkSettings(ApkSettings apkSettings) {
286 mApkSettings = apkSettings;
289 public ApkSettings getApkSettings() {
294 * Convenience method returning all the IProject objects for the resolved libraries.
295 * <p/>If some dependencies are not resolved (or their projects is not opened in Eclipse),
296 * they will not show up in this list.
297 * @return the resolved projects or null if there are no project (either no resolved or no
300 public IProject[] getLibraryProjects() {
301 return mLibraryProjects;
305 * Returns whether this is a library project.
307 public boolean isLibrary() {
308 String value = mProperties.getProperty(ProjectProperties.PROPERTY_LIBRARY);
309 return value != null && Boolean.valueOf(value);
313 * Returns whether the project is missing some required libraries.
315 public boolean isMissingLibraries() {
316 synchronized (mLibraries) {
317 for (LibraryState state : mLibraries) {
318 if (state.getProjectState() == null) {
328 * Returns the {@link LibraryState} object for a given {@link IProject}.
329 * </p>This can only return a non-null object if the link between the main project's
330 * {@link IProject} and the library's {@link IProject} was done.
332 * @return the matching LibraryState or <code>null</code>
334 * @see #needs(IProject)
336 public LibraryState getLibrary(IProject library) {
337 synchronized (mLibraries) {
338 for (LibraryState state : mLibraries) {
339 if (state.getProjectState().equals(library)) {
348 public LibraryState getLibrary(String name) {
349 synchronized (mLibraries) {
350 for (LibraryState state : mLibraries) {
351 if (state.getProjectState().getProject().getName().equals(name)) {
362 * Returns whether a given library project is needed by the receiver.
363 * <p/>If the library is needed, this finds the matching {@link LibraryState}, initializes it
364 * so that it contains the library's {@link IProject} object (so that
365 * {@link LibraryState#getProjectState()} does not return null) and then returns it.
367 * @param libraryProject the library project to check.
368 * @return a non null object if the project is a library dependency,
369 * <code>null</code> otherwise.
371 * @see LibraryState#getProjectState()
373 public LibraryState needs(ProjectState libraryProject) {
374 // compute current location
375 File projectFile = new File(mProject.getLocation().toOSString());
377 // get the location of the library.
378 File libraryFile = new File(libraryProject.getProject().getLocation().toOSString());
380 // loop on all libraries and check if the path match
381 synchronized (mLibraries) {
382 for (LibraryState state : mLibraries) {
383 if (state.getProjectState() == null) {
384 File library = new File(projectFile, state.getRelativePath());
386 File absPath = library.getCanonicalFile();
387 if (absPath.equals(libraryFile)) {
388 state.setProject(libraryProject);
391 } catch (IOException e) {
392 // ignore this library
402 * Updates a library with a new path.
403 * <p/>This method acts both as a check and an action. If the project does not depend on the
404 * given <var>oldRelativePath</var> then no action is done and <code>null</code> is returned.
405 * <p/>If the project depends on the library, then the project is updated with the new path,
406 * and the {@link LibraryState} for the library is returned.
407 * <p/>Updating the project does two things:<ul>
408 * <li>Update LibraryState with new relative path and new {@link IProject} object.</li>
409 * <li>Update the main project's <code>default.properties</code> with the new relative path
410 * for the changed library.</li>
413 * @param oldRelativePath the old library path relative to this project
414 * @param newRelativePath the new library path relative to this project
415 * @param newLibraryState the new {@link ProjectState} object.
416 * @return a non null object if the project depends on the library.
418 * @see LibraryState#getProjectState()
420 public LibraryState updateLibrary(String oldRelativePath, String newRelativePath,
421 ProjectState newLibraryState) {
422 // compute current location
423 File projectFile = new File(mProject.getLocation().toOSString());
425 // loop on all libraries and check if the path matches
426 synchronized (mLibraries) {
427 for (LibraryState state : mLibraries) {
428 if (state.getProjectState() == null) {
430 // oldRelativePath may not be the same exact string as the
431 // one in the project properties (trailing separator could be different
433 // Use java.io.File to deal with this and also do a platform-dependent
435 File library1 = new File(projectFile, oldRelativePath);
436 File library2 = new File(projectFile, state.getRelativePath());
437 if (library1.getCanonicalPath().equals(library2.getCanonicalPath())) {
438 // save the exact property string to replace.
439 String oldProperty = state.getRelativePath();
441 // then update the LibraryPath.
442 state.setRelativePath(newRelativePath);
443 state.setProject(newLibraryState);
445 // update the default.properties file
446 IStatus status = replaceLibraryProperty(oldProperty, newRelativePath);
447 if (status != null) {
448 if (status.getSeverity() != IStatus.OK) {
449 // log the error somehow.
452 // This should not happen since the library wouldn't be here in the
456 // return the LibraryState object.
459 } catch (IOException e) {
460 // ignore this library
469 private IStatus replaceLibraryProperty(String oldValue, String newValue) {
472 String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index++);
473 String rootPath = mProperties.getProperty(propName);
475 if (rootPath == null) {
479 if (rootPath.equals(oldValue)) {
480 mProperties.setProperty(propName, newValue);
483 } catch (IOException e) {
484 return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
485 String.format("Failed to save %1$s for project %2$s",
486 mProperties.getType().getFilename(), mProject.getName()),
489 return Status.OK_STATUS;
496 private void updateLibraries() {
497 ArrayList<IProject> list = new ArrayList<IProject>();
498 synchronized (mLibraries) {
499 for (LibraryState state : mLibraries) {
500 if (state.getProjectState() != null) {
501 list.add(state.getProjectState().getProject());
506 mLibraryProjects = list.toArray(new IProject[list.size()]);
510 * Converts a path containing only / by the proper platform separator.
512 private String convertPath(String path) {
513 return path.replaceAll("/", File.separator); //$NON-NLS-1$
517 public boolean equals(Object obj) {
518 if (obj instanceof ProjectState) {
519 return mProject.equals(((ProjectState) obj).mProject);
520 } else if (obj instanceof IProject) {
521 return mProject.equals(obj);
528 public int hashCode() {
529 return mProject.hashCode();