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