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