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