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