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.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 
     70         private LibraryState(String relativePath) {
     71             mRelativePath = relativePath;
     72         }
     73 
     74         /**
     75          * Returns the {@link ProjectState} of the main project using this library.
     76          */
     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          */
     87         public void close() {
     88             mProjectState.removeParentProject(getMainProjectState());
     89             mProjectState = null;
     90             mPath = null;
     91 
     92             getMainProjectState().updateFullLibraryList();
     93         }
     94 
     95         private void setRelativePath(String relativePath) {
     96             mRelativePath = relativePath;
     97         }
     98 
     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          */
    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          */
    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          */
    130         public String getProjectLocation() {
    131             return mPath;
    132         }
    133 
    134         @Override
    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
    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 
    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 
    202     public IProject getProject() {
    203         return mProject;
    204     }
    205 
    206     public ProjectProperties getProperties() {
    207         return mProperties;
    208     }
    209 
    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      */
    222     public String getTargetHashString() {
    223         if (mTarget != null) {
    224             return mTarget.hashString();
    225         }
    226 
    227         return mProperties.getProperty(ProjectProperties.PROPERTY_TARGET);
    228     }
    229 
    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 
    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      */
    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 
    314     public void setApkSettings(ApkSettings apkSettings) {
    315         mApkSettings = apkSettings;
    316     }
    317 
    318     public ApkSettings getApkSettings() {
    319         return mApkSettings;
    320     }
    321 
    322     /**
    323      * Returns the list of {@link LibraryState}.
    324      */
    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      */
    339     public IProject[] getFullLibraryProjects() {
    340         return mLibraryProjects;
    341     }
    342 
    343     /**
    344      * Returns whether this is a library project.
    345      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    549     private void addParentProject(ProjectState parentState) {
    550         mParentProjects.add(parentState);
    551     }
    552 
    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      */
    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      */
    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      */
    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      */
    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      */
    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
    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
    675     public int hashCode() {
    676         return mProject.hashCode();
    677     }
    678 
    679     @Override
    680     public String toString() {
    681         return mProject.getName();
    682     }
    683 }
    684