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