Home | History | Annotate | Download | only in sdk
      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 
     72         private LibraryState(String relativePath) {
     73             mRelativePath = relativePath;
     74         }
     75 
     76         /**
     77          * Returns the {@link ProjectState} of the main project using this library.
     78          */
     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          */
     89         public void close() {
     90             mProjectState.removeParentProject(getMainProjectState());
     91             mProjectState = null;
     92             mPath = null;
     93 
     94             getMainProjectState().updateFullLibraryList();
     95         }
     96 
     97         private void setRelativePath(String relativePath) {
     98             mRelativePath = relativePath;
     99         }
    100 
    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          */
    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          */
    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          */
    132         public String getProjectLocation() {
    133             return mPath;
    134         }
    135 
    136         @Override
    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
    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 
    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 
    201     public IProject getProject() {
    202         return mProject;
    203     }
    204 
    205     public ProjectProperties getProperties() {
    206         return mProperties;
    207     }
    208 
    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      */
    221     public String getTargetHashString() {
    222         if (mTarget != null) {
    223             return mTarget.hashString();
    224         }
    225 
    226         return mProperties.getProperty(ProjectProperties.PROPERTY_TARGET);
    227     }
    228 
    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 
    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      */
    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      */
    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      */
    330     public List<IProject> getFullLibraryProjects() {
    331         return mLibraryProjects;
    332     }
    333 
    334     /**
    335      * Returns whether this is a library project.
    336      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    540     private void addParentProject(ProjectState parentState) {
    541         mParentProjects.add(parentState);
    542     }
    543 
    544     private void removeParentProject(ProjectState parentState) {
    545         mParentProjects.remove(parentState);
    546     }
    547 
    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      */
    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 */
    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      */
    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      */
    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      */
    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      */
    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      */
    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
    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
    694     public int hashCode() {
    695         return mProject.hashCode();
    696     }
    697 
    698     @Override
    699     public String toString() {
    700         return mProject.getName();
    701     }
    702 }
    703