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