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