• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
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
7  *
8  *      http://www.eclipse.org/org/documents/epl-v10.php
9  *
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.
15  */
16 
17 package com.android.ide.eclipse.adt.internal.sdk;
18 
19 import com.android.ide.eclipse.adt.AdtPlugin;
20 import com.android.sdklib.IAndroidTarget;
21 import com.android.sdklib.internal.project.ApkSettings;
22 import com.android.sdklib.internal.project.ProjectProperties;
23 import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy;
24 
25 import org.eclipse.core.resources.IProject;
26 import org.eclipse.core.runtime.IStatus;
27 import org.eclipse.core.runtime.Status;
28 
29 import java.io.File;
30 import java.io.IOException;
31 import java.util.ArrayList;
32 import java.util.Collections;
33 import java.util.List;
34 import java.util.regex.Matcher;
35 
36 /**
37  * Centralized state for Android Eclipse project.
38  * <p>This gives raw access to the properties (from <code>default.properties</code>), as well
39  * as direct access to target, apksettings and library information.
40  *
41  * This also gives access to library information.
42  *
43  * {@link #isLibrary()} indicates if the project is a library.
44  * {@link #hasLibraries()} and {@link #getLibraries()} give access to the libraries through
45  * instances of {@link LibraryState}. A {@link LibraryState} instance is a link between a main
46  * project and its library. Theses instances are owned by the {@link ProjectState}.
47  *
48  * {@link #isMissingLibraries()} will indicate if the project has libraries that are not resolved.
49  * Unresolved libraries are libraries that do not have any matching opened Eclipse project.
50  * When there are missing libraries, the {@link LibraryState} instance for them will return null
51  * for {@link LibraryState#getProjectState()}.
52  *
53  */
54 public final class ProjectState {
55 
56     /**
57      * A class that represents a library linked to a project.
58      * <p/>It does not represent the library uniquely. Instead the {@link LibraryState} is linked
59      * to the main project which is accessible through {@link #getMainProjectState()}.
60      * <p/>If a library is used by two different projects, then there will be two different
61      * instances of {@link LibraryState} for the library.
62      *
63      * @see ProjectState#getLibrary(IProject)
64      */
65     public final class LibraryState {
66         private String mRelativePath;
67         private ProjectState mProjectState;
68         private String mPath;
69 
LibraryState(String relativePath)70         private LibraryState(String relativePath) {
71             mRelativePath = relativePath;
72         }
73 
74         /**
75          * Returns the {@link ProjectState} of the main project using this library.
76          */
getMainProjectState()77         public ProjectState getMainProjectState() {
78             return ProjectState.this;
79         }
80 
81         /**
82          * Closes the library. This resets the IProject from this object ({@link #getProjectState()} will
83          * return <code>null</code>), and updates the main project data so that the library
84          * {@link IProject} object does not show up in the return value of
85          * {@link ProjectState#getFullLibraryProjects()}.
86          */
close()87         public void close() {
88             mProjectState.removeParentProject(getMainProjectState());
89             mProjectState = null;
90             mPath = null;
91 
92             getMainProjectState().updateFullLibraryList();
93         }
94 
setRelativePath(String relativePath)95         private void setRelativePath(String relativePath) {
96             mRelativePath = relativePath;
97         }
98 
setProject(ProjectState project)99         private void setProject(ProjectState project) {
100             mProjectState = project;
101             mPath = project.getProject().getLocation().toOSString();
102             mProjectState.addParentProject(getMainProjectState());
103 
104             getMainProjectState().updateFullLibraryList();
105         }
106 
107         /**
108          * Returns the relative path of the library from the main project.
109          * <p/>This is identical to the value defined in the main project's default.properties.
110          */
getRelativePath()111         public String getRelativePath() {
112             return mRelativePath;
113         }
114 
115         /**
116          * Returns the {@link ProjectState} item for the library. This can be null if the project
117          * is not actually opened in Eclipse.
118          */
getProjectState()119         public ProjectState getProjectState() {
120             return mProjectState;
121         }
122 
123         /**
124          * Returns the OS-String location of the library project.
125          * <p/>This is based on location of the Eclipse project that matched
126          * {@link #getRelativePath()}.
127          *
128          * @return The project location, or null if the project is not opened in Eclipse.
129          */
getProjectLocation()130         public String getProjectLocation() {
131             return mPath;
132         }
133 
134         @Override
equals(Object obj)135         public boolean equals(Object obj) {
136             if (obj instanceof LibraryState) {
137                 // the only thing that's always non-null is the relative path.
138                 LibraryState objState = (LibraryState)obj;
139                 return mRelativePath.equals(objState.mRelativePath) &&
140                         getMainProjectState().equals(objState.getMainProjectState());
141             } else if (obj instanceof ProjectState || obj instanceof IProject) {
142                 return mProjectState != null && mProjectState.equals(obj);
143             } else if (obj instanceof String) {
144                 return normalizePath(mRelativePath).equals(normalizePath((String) obj));
145             }
146 
147             return false;
148         }
149 
150         @Override
hashCode()151         public int hashCode() {
152             return mRelativePath.hashCode();
153         }
154     }
155 
156     private final IProject mProject;
157     private final ProjectProperties mProperties;
158     private IAndroidTarget mTarget;
159     private ApkSettings mApkSettings;
160     /**
161      * list of libraries. Access to this list must be protected by
162      * <code>synchronized(mLibraries)</code>, but it is important that such code do not call
163      * out to other classes (especially those protected by {@link Sdk#getLock()}.)
164      */
165     private final ArrayList<LibraryState> mLibraries = new ArrayList<LibraryState>();
166     /** Cached list of all IProject instances representing the resolved libraries, including
167      * indirect dependencies. This must never be null. */
168     private IProject[] mLibraryProjects = new IProject[0];
169     /**
170      * List of parent projects. When this instance is a library ({@link #isLibrary()} returns
171      * <code>true</code>) then this is filled with projects that depends on this project.
172      */
173     private final ArrayList<ProjectState> mParentProjects = new ArrayList<ProjectState>();
174 
ProjectState(IProject project, ProjectProperties properties)175     ProjectState(IProject project, ProjectProperties properties) {
176         if (project == null || properties == null) {
177             throw new NullPointerException();
178         }
179 
180         mProject = project;
181         mProperties = properties;
182 
183         // load the ApkSettings
184         mApkSettings = new ApkSettings(properties);
185 
186         // load the libraries
187         synchronized (mLibraries) {
188             int index = 1;
189             while (true) {
190                 String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index++);
191                 String rootPath = mProperties.getProperty(propName);
192 
193                 if (rootPath == null) {
194                     break;
195                 }
196 
197                 mLibraries.add(new LibraryState(convertPath(rootPath)));
198             }
199         }
200     }
201 
getProject()202     public IProject getProject() {
203         return mProject;
204     }
205 
getProperties()206     public ProjectProperties getProperties() {
207         return mProperties;
208     }
209 
setTarget(IAndroidTarget target)210     public void setTarget(IAndroidTarget target) {
211         mTarget = target;
212     }
213 
214     /**
215      * Returns the project's target's hash string.
216      * <p/>If {@link #getTarget()} returns a valid object, then this returns the value of
217      * {@link IAndroidTarget#hashString()}.
218      * <p/>Otherwise this will return the value of the property
219      * {@link ProjectProperties#PROPERTY_TARGET} from {@link #getProperties()} (if valid).
220      * @return the target hash string or null if not found.
221      */
getTargetHashString()222     public String getTargetHashString() {
223         if (mTarget != null) {
224             return mTarget.hashString();
225         }
226 
227         return mProperties.getProperty(ProjectProperties.PROPERTY_TARGET);
228     }
229 
getTarget()230     public IAndroidTarget getTarget() {
231         return mTarget;
232     }
233 
234     public static class LibraryDifference {
235         public boolean removed = false;
236         public boolean added = false;
237 
hasDiff()238         public boolean hasDiff() {
239             return removed || added;
240         }
241     }
242 
243     /**
244      * Reloads the content of the properties.
245      * <p/>This also reset the reference to the target as it may have changed, therefore this
246      * should be followed by a call to {@link Sdk#loadTarget(ProjectState)}.
247      *
248      * <p/>If the project libraries changes, they are updated to a certain extent.<br>
249      * Removed libraries are removed from the state list, and added to the {@link LibraryDifference}
250      * object that is returned so that they can be processed.<br>
251      * Added libraries are added to the state (as new {@link LibraryState} objects), but their
252      * IProject is not resolved. {@link ProjectState#needs(ProjectState)} should be called
253      * afterwards to properly initialize the libraries.
254      *
255      * @return an instance of {@link LibraryDifference} describing the change in libraries.
256      */
reloadProperties()257     public LibraryDifference reloadProperties() {
258         mTarget = null;
259         mProperties.reload();
260 
261         // compare/reload the libraries.
262 
263         // if the order change it won't impact the java part, so instead try to detect removed/added
264         // libraries.
265 
266         LibraryDifference diff = new LibraryDifference();
267 
268         synchronized (mLibraries) {
269             List<LibraryState> oldLibraries = new ArrayList<LibraryState>(mLibraries);
270             mLibraries.clear();
271 
272             // load the libraries
273             int index = 1;
274             while (true) {
275                 String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index++);
276                 String rootPath = mProperties.getProperty(propName);
277 
278                 if (rootPath == null) {
279                     break;
280                 }
281 
282                 // search for a library with the same path (not exact same string, but going
283                 // to the same folder).
284                 String convertedPath = convertPath(rootPath);
285                 boolean found = false;
286                 for (int i = 0 ; i < oldLibraries.size(); i++) {
287                     LibraryState libState = oldLibraries.get(i);
288                     if (libState.equals(convertedPath)) {
289                         // it's a match. move it back to mLibraries and remove it from the
290                         // old library list.
291                         found = true;
292                         mLibraries.add(libState);
293                         oldLibraries.remove(i);
294                         break;
295                     }
296                 }
297 
298                 if (found == false) {
299                     diff.added = true;
300                     mLibraries.add(new LibraryState(convertedPath));
301                 }
302             }
303 
304             // whatever's left in oldLibraries is removed.
305             diff.removed = oldLibraries.size() > 0;
306 
307             // update the library with what IProjet are known at the time.
308             updateFullLibraryList();
309         }
310 
311         return diff;
312     }
313 
setApkSettings(ApkSettings apkSettings)314     public void setApkSettings(ApkSettings apkSettings) {
315         mApkSettings = apkSettings;
316     }
317 
getApkSettings()318     public ApkSettings getApkSettings() {
319         return mApkSettings;
320     }
321 
322     /**
323      * Returns the list of {@link LibraryState}.
324      */
getLibraries()325     public List<LibraryState> getLibraries() {
326         synchronized (mLibraries) {
327             return Collections.unmodifiableList(mLibraries);
328         }
329     }
330 
331     /**
332      * Returns all the <strong>resolved</strong> library projects, including indirect dependencies.
333      * The array is ordered to match the library priority order for resource processing with
334      * <code>aapt</code>.
335      * <p/>If some dependencies are not resolved (or their projects is not opened in Eclipse),
336      * they will not show up in this list.
337      * @return the resolved projects. May be an empty list.
338      */
getFullLibraryProjects()339     public IProject[] getFullLibraryProjects() {
340         return mLibraryProjects;
341     }
342 
343     /**
344      * Returns whether this is a library project.
345      */
isLibrary()346     public boolean isLibrary() {
347         String value = mProperties.getProperty(ProjectProperties.PROPERTY_LIBRARY);
348         return value != null && Boolean.valueOf(value);
349     }
350 
351     /**
352      * Returns whether the project depends on one or more libraries.
353      */
hasLibraries()354     public boolean hasLibraries() {
355         synchronized (mLibraries) {
356             return mLibraries.size() > 0;
357         }
358     }
359 
360     /**
361      * Returns whether the project is missing some required libraries.
362      */
isMissingLibraries()363     public boolean isMissingLibraries() {
364         synchronized (mLibraries) {
365             for (LibraryState state : mLibraries) {
366                 if (state.getProjectState() == null) {
367                     return true;
368                 }
369             }
370         }
371 
372         return false;
373     }
374 
375     /**
376      * Returns the {@link LibraryState} object for a given {@link IProject}.
377      * </p>This can only return a non-null object if the link between the main project's
378      * {@link IProject} and the library's {@link IProject} was done.
379      *
380      * @return the matching LibraryState or <code>null</code>
381      *
382      * @see #needs(IProject)
383      */
getLibrary(IProject library)384     public LibraryState getLibrary(IProject library) {
385         synchronized (mLibraries) {
386             for (LibraryState state : mLibraries) {
387                 ProjectState ps = state.getProjectState();
388                 if (ps != null && ps.equals(library)) {
389                     return state;
390                 }
391             }
392         }
393 
394         return null;
395     }
396 
397     /**
398      * Returns the {@link LibraryState} object for a given <var>name</var>.
399      * </p>This can only return a non-null object if the link between the main project's
400      * {@link IProject} and the library's {@link IProject} was done.
401      *
402      * @return the matching LibraryState or <code>null</code>
403      *
404      * @see #needs(IProject)
405      */
getLibrary(String name)406     public LibraryState getLibrary(String name) {
407         synchronized (mLibraries) {
408             for (LibraryState state : mLibraries) {
409                 ProjectState ps = state.getProjectState();
410                 if (ps != null && ps.getProject().getName().equals(name)) {
411                     return state;
412                 }
413             }
414         }
415 
416         return null;
417     }
418 
419 
420     /**
421      * Returns whether a given library project is needed by the receiver.
422      * <p/>If the library is needed, this finds the matching {@link LibraryState}, initializes it
423      * so that it contains the library's {@link IProject} object (so that
424      * {@link LibraryState#getProjectState()} does not return null) and then returns it.
425      *
426      * @param libraryProject the library project to check.
427      * @return a non null object if the project is a library dependency,
428      * <code>null</code> otherwise.
429      *
430      * @see LibraryState#getProjectState()
431      */
needs(ProjectState libraryProject)432     public LibraryState needs(ProjectState libraryProject) {
433         // compute current location
434         File projectFile = mProject.getLocation().toFile();
435 
436         // get the location of the library.
437         File libraryFile = libraryProject.getProject().getLocation().toFile();
438 
439         // loop on all libraries and check if the path match
440         synchronized (mLibraries) {
441             for (LibraryState state : mLibraries) {
442                 if (state.getProjectState() == null) {
443                     File library = new File(projectFile, state.getRelativePath());
444                     try {
445                         File absPath = library.getCanonicalFile();
446                         if (absPath.equals(libraryFile)) {
447                             state.setProject(libraryProject);
448                             return state;
449                         }
450                     } catch (IOException e) {
451                         // ignore this library
452                     }
453                 }
454             }
455         }
456 
457         return null;
458     }
459 
460     /**
461      * Returns whether the project depends on a given <var>library</var>
462      * @param library the library to check.
463      * @return true if the project depends on the library. This is not affected by whether the link
464      * was done through {@link #needs(ProjectState)}.
465      */
dependsOn(ProjectState library)466     public boolean dependsOn(ProjectState library) {
467         synchronized (mLibraries) {
468             for (LibraryState state : mLibraries) {
469                 if (state != null && state.getProjectState() != null &&
470                         library.getProject().equals(state.getProjectState().getProject())) {
471                     return true;
472                 }
473             }
474         }
475 
476         return false;
477     }
478 
479 
480     /**
481      * Updates a library with a new path.
482      * <p/>This method acts both as a check and an action. If the project does not depend on the
483      * given <var>oldRelativePath</var> then no action is done and <code>null</code> is returned.
484      * <p/>If the project depends on the library, then the project is updated with the new path,
485      * and the {@link LibraryState} for the library is returned.
486      * <p/>Updating the project does two things:<ul>
487      * <li>Update LibraryState with new relative path and new {@link IProject} object.</li>
488      * <li>Update the main project's <code>default.properties</code> with the new relative path
489      * for the changed library.</li>
490      * </ul>
491      *
492      * @param oldRelativePath the old library path relative to this project
493      * @param newRelativePath the new library path relative to this project
494      * @param newLibraryState the new {@link ProjectState} object.
495      * @return a non null object if the project depends on the library.
496      *
497      * @see LibraryState#getProjectState()
498      */
updateLibrary(String oldRelativePath, String newRelativePath, ProjectState newLibraryState)499     public LibraryState updateLibrary(String oldRelativePath, String newRelativePath,
500             ProjectState newLibraryState) {
501         // compute current location
502         File projectFile = mProject.getLocation().toFile();
503 
504         // loop on all libraries and check if the path matches
505         synchronized (mLibraries) {
506             for (LibraryState state : mLibraries) {
507                 if (state.getProjectState() == null) {
508                     try {
509                         // oldRelativePath may not be the same exact string as the
510                         // one in the project properties (trailing separator could be different
511                         // for instance).
512                         // Use java.io.File to deal with this and also do a platform-dependent
513                         // path comparison
514                         File library1 = new File(projectFile, oldRelativePath);
515                         File library2 = new File(projectFile, state.getRelativePath());
516                         if (library1.getCanonicalPath().equals(library2.getCanonicalPath())) {
517                             // save the exact property string to replace.
518                             String oldProperty = state.getRelativePath();
519 
520                             // then update the LibraryPath.
521                             state.setRelativePath(newRelativePath);
522                             state.setProject(newLibraryState);
523 
524                             // update the default.properties file
525                             IStatus status = replaceLibraryProperty(oldProperty, newRelativePath);
526                             if (status != null) {
527                                 if (status.getSeverity() != IStatus.OK) {
528                                     // log the error somehow.
529                                 }
530                             } else {
531                                 // This should not happen since the library wouldn't be here in the
532                                 // first place
533                             }
534 
535                             // return the LibraryState object.
536                             return state;
537                         }
538                     } catch (IOException e) {
539                         // ignore this library
540                     }
541                 }
542             }
543         }
544 
545         return null;
546     }
547 
548 
addParentProject(ProjectState parentState)549     private void addParentProject(ProjectState parentState) {
550         mParentProjects.add(parentState);
551     }
552 
removeParentProject(ProjectState parentState)553     private void removeParentProject(ProjectState parentState) {
554         mParentProjects.remove(parentState);
555     }
556 
557     /**
558      * Update the value of a library dependency.
559      * <p/>This loops on all current dependency looking for the value to replace and then replaces
560      * it.
561      * <p/>This both updates the in-memory {@link #mProperties} values and on-disk
562      * default.properties file.
563      * @param oldValue the old value to replace
564      * @param newValue the new value to set.
565      * @return the status of the replacement. If null, no replacement was done (value not found).
566      */
replaceLibraryProperty(String oldValue, String newValue)567     private IStatus replaceLibraryProperty(String oldValue, String newValue) {
568         int index = 1;
569         while (true) {
570             String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index++);
571             String rootPath = mProperties.getProperty(propName);
572 
573             if (rootPath == null) {
574                 break;
575             }
576 
577             if (rootPath.equals(oldValue)) {
578                 // need to update the properties. Get a working copy to change it and save it on
579                 // disk since ProjectProperties is read-only.
580                 ProjectPropertiesWorkingCopy workingCopy = mProperties.makeWorkingCopy();
581                 workingCopy.setProperty(propName, newValue);
582                 try {
583                     workingCopy.save();
584 
585                     // reload the properties with the new values from the disk.
586                     mProperties.reload();
587                 } catch (Exception e) {
588                     return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, String.format(
589                             "Failed to save %1$s for project %2$s",
590                                     mProperties.getType() .getFilename(), mProject.getName()),
591                             e);
592 
593                 }
594                 return Status.OK_STATUS;
595             }
596         }
597 
598         return null;
599     }
600 
601     /**
602      * Update the full library list, including indirect dependencies. The result is returned by
603      * {@link #getFullLibraryProjects()}.
604      */
updateFullLibraryList()605     void updateFullLibraryList() {
606         ArrayList<IProject> list = new ArrayList<IProject>();
607         synchronized (mLibraries) {
608             buildFullLibraryDependencies(mLibraries, list);
609         }
610 
611         mLibraryProjects = list.toArray(new IProject[list.size()]);
612     }
613 
614     /**
615      * Resolves a given list of libraries, finds out if they depend on other libraries, and
616      * returns a full list of all the direct and indirect dependencies in the proper order (first
617      * is higher priority when calling aapt).
618      * @param inLibraries the libraries to resolve
619      * @param outLibraries where to store all the libraries.
620      */
buildFullLibraryDependencies(List<LibraryState> inLibraries, ArrayList<IProject> outLibraries)621     private void buildFullLibraryDependencies(List<LibraryState> inLibraries,
622             ArrayList<IProject> outLibraries) {
623         // loop in the inverse order to resolve dependencies on the libraries, so that if a library
624         // is required by two higher level libraries it can be inserted in the correct place
625         for (int i = inLibraries.size() - 1  ; i >= 0 ; i--) {
626             LibraryState library = inLibraries.get(i);
627 
628             // get its libraries if possible
629             ProjectState libProjectState = library.getProjectState();
630             if (libProjectState != null) {
631                 List<LibraryState> dependencies = libProjectState.getLibraries();
632 
633                 // build the dependencies for those libraries
634                 buildFullLibraryDependencies(dependencies, outLibraries);
635 
636                 // and add the current library (if needed) in front (higher priority)
637                 if (outLibraries.contains(libProjectState.getProject()) == false) {
638                     outLibraries.add(0, libProjectState.getProject());
639                 }
640             }
641         }
642     }
643 
644 
645     /**
646      * Converts a path containing only / by the proper platform separator.
647      */
convertPath(String path)648     private String convertPath(String path) {
649         return path.replaceAll("/", Matcher.quoteReplacement(File.separator)); //$NON-NLS-1$
650     }
651 
652     /**
653      * Normalizes a relative path.
654      */
normalizePath(String path)655     private String normalizePath(String path) {
656         path = convertPath(path);
657         if (path.endsWith("/")) { //$NON-NLS-1$
658             path = path.substring(0, path.length() - 1);
659         }
660         return path;
661     }
662 
663     @Override
equals(Object obj)664     public boolean equals(Object obj) {
665         if (obj instanceof ProjectState) {
666             return mProject.equals(((ProjectState) obj).mProject);
667         } else if (obj instanceof IProject) {
668             return mProject.equals(obj);
669         }
670 
671         return false;
672     }
673 
674     @Override
hashCode()675     public int hashCode() {
676         return mProject.hashCode();
677     }
678 
679     @Override
toString()680     public String toString() {
681         return mProject.getName();
682     }
683 }
684