1 /* 2 * Copyright (C) 2007 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.common.sdk.LoadStatus; 20 import com.android.ide.eclipse.adt.AdtConstants; 21 import com.android.ide.eclipse.adt.AdtPlugin; 22 import com.android.ide.eclipse.adt.internal.sdk.ProjectState; 23 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 24 import com.android.sdklib.AndroidVersion; 25 import com.android.sdklib.IAndroidTarget; 26 import com.android.sdklib.SdkConstants; 27 import com.android.sdklib.IAndroidTarget.IOptionalLibrary; 28 29 import org.eclipse.core.resources.IMarker; 30 import org.eclipse.core.resources.IProject; 31 import org.eclipse.core.resources.IResource; 32 import org.eclipse.core.resources.IWorkspaceRoot; 33 import org.eclipse.core.resources.ResourcesPlugin; 34 import org.eclipse.core.runtime.CoreException; 35 import org.eclipse.core.runtime.FileLocator; 36 import org.eclipse.core.runtime.IPath; 37 import org.eclipse.core.runtime.IProgressMonitor; 38 import org.eclipse.core.runtime.IStatus; 39 import org.eclipse.core.runtime.NullProgressMonitor; 40 import org.eclipse.core.runtime.Path; 41 import org.eclipse.core.runtime.Platform; 42 import org.eclipse.core.runtime.Status; 43 import org.eclipse.core.runtime.jobs.Job; 44 import org.eclipse.jdt.core.ClasspathContainerInitializer; 45 import org.eclipse.jdt.core.IAccessRule; 46 import org.eclipse.jdt.core.IClasspathAttribute; 47 import org.eclipse.jdt.core.IClasspathContainer; 48 import org.eclipse.jdt.core.IClasspathEntry; 49 import org.eclipse.jdt.core.IJavaModel; 50 import org.eclipse.jdt.core.IJavaProject; 51 import org.eclipse.jdt.core.JavaCore; 52 import org.eclipse.jdt.core.JavaModelException; 53 import org.osgi.framework.Bundle; 54 55 import java.io.File; 56 import java.io.IOException; 57 import java.io.InputStream; 58 import java.net.URI; 59 import java.net.URISyntaxException; 60 import java.net.URL; 61 import java.util.ArrayList; 62 import java.util.HashSet; 63 import java.util.regex.Pattern; 64 65 /** 66 * Classpath container initializer responsible for binding {@link AndroidClasspathContainer} to 67 * {@link IProject}s. This removes the hard-coded path to the android.jar. 68 */ 69 public class AndroidClasspathContainerInitializer extends ClasspathContainerInitializer { 70 71 public static final String NULL_API_URL = "<null>"; //$NON-NLS-1$ 72 73 public static final String SOURCES_ZIP = "/sources.zip"; //$NON-NLS-1$ 74 75 public static final String COM_ANDROID_IDE_ECLIPSE_ADT_SOURCE = 76 "com.android.ide.eclipse.source"; //$NON-NLS-1$ 77 78 private static final String ANDROID_API_REFERENCE = 79 "http://developer.android.com/reference/"; //$NON-NLS-1$ 80 81 private final static String PROPERTY_ANDROID_API = "androidApi"; //$NON-NLS-1$ 82 83 private final static String PROPERTY_ANDROID_SOURCE = "androidSource"; //$NON-NLS-1$ 84 85 /** path separator to store multiple paths in a single property. This is guaranteed to not 86 * be in a path. 87 */ 88 private final static String PATH_SEPARATOR = "\u001C"; //$NON-NLS-1$ 89 90 private final static String PROPERTY_CONTAINER_CACHE = "androidContainerCache"; //$NON-NLS-1$ 91 private final static String PROPERTY_TARGET_NAME = "androidTargetCache"; //$NON-NLS-1$ 92 private final static String CACHE_VERSION = "01"; //$NON-NLS-1$ 93 private final static String CACHE_VERSION_SEP = CACHE_VERSION + PATH_SEPARATOR; 94 95 private final static int CACHE_INDEX_JAR = 0; 96 private final static int CACHE_INDEX_SRC = 1; 97 private final static int CACHE_INDEX_DOCS_URI = 2; 98 private final static int CACHE_INDEX_OPT_DOCS_URI = 3; 99 private final static int CACHE_INDEX_ADD_ON_START = CACHE_INDEX_OPT_DOCS_URI; 100 101 public AndroidClasspathContainerInitializer() { 102 // pass 103 } 104 105 /** 106 * Binds a classpath container to a {@link IClasspathContainer} for a given project, 107 * or silently fails if unable to do so. 108 * @param containerPath the container path that is the container id. 109 * @param project the project to bind 110 */ 111 @Override 112 public void initialize(IPath containerPath, IJavaProject project) throws CoreException { 113 if (AdtConstants.CONTAINER_FRAMEWORK.equals(containerPath.toString())) { 114 IClasspathContainer container = allocateAndroidContainer(project); 115 if (container != null) { 116 JavaCore.setClasspathContainer(new Path(AdtConstants.CONTAINER_FRAMEWORK), 117 new IJavaProject[] { project }, 118 new IClasspathContainer[] { container }, 119 new NullProgressMonitor()); 120 } 121 } 122 } 123 124 /** 125 * Updates the {@link IJavaProject} objects with new android framework container. This forces 126 * JDT to recompile them. 127 * @param androidProjects the projects to update. 128 * @return <code>true</code> if success, <code>false</code> otherwise. 129 */ 130 public static boolean updateProjects(IJavaProject[] androidProjects) { 131 try { 132 // Allocate a new AndroidClasspathContainer, and associate it to the android framework 133 // container id for each projects. 134 // By providing a new association between a container id and a IClasspathContainer, 135 // this forces the JDT to query the IClasspathContainer for new IClasspathEntry (with 136 // IClasspathContainer#getClasspathEntries()), and therefore force recompilation of 137 // the projects. 138 int projectCount = androidProjects.length; 139 140 IClasspathContainer[] containers = new IClasspathContainer[projectCount]; 141 for (int i = 0 ; i < projectCount; i++) { 142 containers[i] = allocateAndroidContainer(androidProjects[i]); 143 } 144 145 // give each project their new container in one call. 146 JavaCore.setClasspathContainer( 147 new Path(AdtConstants.CONTAINER_FRAMEWORK), 148 androidProjects, containers, new NullProgressMonitor()); 149 150 return true; 151 } catch (JavaModelException e) { 152 return false; 153 } 154 } 155 156 /** 157 * Allocates and returns an {@link AndroidClasspathContainer} object with the proper 158 * path to the framework jar file. 159 * @param javaProject The java project that will receive the container. 160 */ 161 private static IClasspathContainer allocateAndroidContainer(IJavaProject javaProject) { 162 final IProject iProject = javaProject.getProject(); 163 164 String markerMessage = null; 165 boolean outputToConsole = true; 166 IAndroidTarget target = null; 167 168 try { 169 AdtPlugin plugin = AdtPlugin.getDefault(); 170 if (plugin == null) { // This is totally weird, but I've seen it happen! 171 return null; 172 } 173 174 synchronized (Sdk.getLock()) { 175 boolean sdkIsLoaded = plugin.getSdkLoadStatus() == LoadStatus.LOADED; 176 177 // check if the project has a valid target. 178 ProjectState state = Sdk.getProjectState(iProject); 179 if (state == null) { 180 // looks like the project state (project.properties) couldn't be read! 181 markerMessage = String.format( 182 "Project has no %1$s file! Edit the project properties to set one.", 183 SdkConstants.FN_PROJECT_PROPERTIES); 184 } else { 185 // this might be null if the sdk is not yet loaded. 186 target = state.getTarget(); 187 188 // if we are loaded and the target is non null, we create a valid 189 // ClassPathContainer 190 if (sdkIsLoaded && target != null) { 191 // first make sure the target has loaded its data 192 Sdk.getCurrent().checkAndLoadTargetData(target, null /*project*/); 193 194 String targetName = target.getClasspathName(); 195 196 return new AndroidClasspathContainer( 197 createClasspathEntries(iProject, target, targetName), 198 new Path(AdtConstants.CONTAINER_FRAMEWORK), 199 targetName, 200 IClasspathContainer.K_DEFAULT_SYSTEM); 201 } 202 203 // In case of error, we'll try different thing to provide the best error message 204 // possible. 205 // Get the project's target's hash string (if it exists) 206 String hashString = state.getTargetHashString(); 207 208 if (hashString == null || hashString.length() == 0) { 209 // if there is no hash string we only show this if the SDK is loaded. 210 // For a project opened at start-up with no target, this would be displayed 211 // twice, once when the project is opened, and once after the SDK has 212 // finished loading. 213 // By testing the sdk is loaded, we only show this once in the console. 214 if (sdkIsLoaded) { 215 markerMessage = String.format( 216 "Project has no target set. Edit the project properties to set one."); 217 } 218 } else if (sdkIsLoaded) { 219 markerMessage = String.format( 220 "Unable to resolve target '%s'", hashString); 221 } else { 222 // this is the case where there is a hashString but the SDK is not yet 223 // loaded and therefore we can't get the target yet. 224 // We check if there is a cache of the needed information. 225 AndroidClasspathContainer container = getContainerFromCache(iProject, target); 226 227 if (container == null) { 228 // either the cache was wrong (ie folder does not exists anymore), or 229 // there was no cache. In this case we need to make sure the project 230 // is resolved again after the SDK is loaded. 231 plugin.setProjectToResolve(javaProject); 232 233 markerMessage = String.format( 234 "Unable to resolve target '%s' until the SDK is loaded.", 235 hashString); 236 237 // let's not log this one to the console as it will happen at 238 // every boot, and it's expected. (we do keep the error marker though). 239 outputToConsole = false; 240 241 } else { 242 // we created a container from the cache, so we register the project 243 // to be checked for cache validity once the SDK is loaded 244 plugin.setProjectToCheck(javaProject); 245 246 // and return the container 247 return container; 248 } 249 } 250 } 251 252 // return a dummy container to replace the one we may have had before. 253 // It'll be replaced by the real when if/when the target is resolved if/when the 254 // SDK finishes loading. 255 return new IClasspathContainer() { 256 public IClasspathEntry[] getClasspathEntries() { 257 return new IClasspathEntry[0]; 258 } 259 260 public String getDescription() { 261 return "Unable to get system library for the project"; 262 } 263 264 public int getKind() { 265 return IClasspathContainer.K_DEFAULT_SYSTEM; 266 } 267 268 public IPath getPath() { 269 return null; 270 } 271 }; 272 } 273 } finally { 274 if (markerMessage != null) { 275 // log the error and put the marker on the project if we can. 276 if (outputToConsole) { 277 AdtPlugin.printErrorToConsole(iProject, markerMessage); 278 } 279 280 try { 281 BaseProjectHelper.markProject(iProject, AdtConstants.MARKER_TARGET, 282 markerMessage, IMarker.SEVERITY_ERROR, IMarker.PRIORITY_HIGH); 283 } catch (CoreException e) { 284 // In some cases, the workspace may be locked for modification when we 285 // pass here. 286 // We schedule a new job to put the marker after. 287 final String fmessage = markerMessage; 288 Job markerJob = new Job("Android SDK: Resolving error markers") { 289 @Override 290 protected IStatus run(IProgressMonitor monitor) { 291 try { 292 BaseProjectHelper.markProject(iProject, 293 AdtConstants.MARKER_TARGET, 294 fmessage, IMarker.SEVERITY_ERROR, 295 IMarker.PRIORITY_HIGH); 296 } catch (CoreException e2) { 297 return e2.getStatus(); 298 } 299 300 return Status.OK_STATUS; 301 } 302 }; 303 304 // build jobs are run after other interactive jobs 305 markerJob.setPriority(Job.BUILD); 306 markerJob.schedule(); 307 } 308 } else { 309 // no error, remove potential MARKER_TARGETs. 310 try { 311 if (iProject.exists()) { 312 iProject.deleteMarkers(AdtConstants.MARKER_TARGET, true, 313 IResource.DEPTH_INFINITE); 314 } 315 } catch (CoreException ce) { 316 // In some cases, the workspace may be locked for modification when we pass 317 // here, so we schedule a new job to put the marker after. 318 Job markerJob = new Job("Android SDK: Resolving error markers") { 319 @Override 320 protected IStatus run(IProgressMonitor monitor) { 321 try { 322 iProject.deleteMarkers(AdtConstants.MARKER_TARGET, true, 323 IResource.DEPTH_INFINITE); 324 } catch (CoreException e2) { 325 return e2.getStatus(); 326 } 327 328 return Status.OK_STATUS; 329 } 330 }; 331 332 // build jobs are run after other interactive jobs 333 markerJob.setPriority(Job.BUILD); 334 markerJob.schedule(); 335 } 336 } 337 } 338 } 339 340 /** 341 * Creates and returns an array of {@link IClasspathEntry} objects for the android 342 * framework and optional libraries. 343 * <p/>This references the OS path to the android.jar and the 344 * java doc directory. This is dynamically created when a project is opened, 345 * and never saved in the project itself, so there's no risk of storing an 346 * obsolete path. 347 * The method also stores the paths used to create the entries in the project persistent 348 * properties. A new {@link AndroidClasspathContainer} can be created from the stored path 349 * using the {@link #getContainerFromCache(IProject)} method. 350 * @param project 351 * @param target The target that contains the libraries. 352 * @param targetName 353 */ 354 private static IClasspathEntry[] createClasspathEntries(IProject project, 355 IAndroidTarget target, String targetName) { 356 357 // get the path from the target 358 String[] paths = getTargetPaths(target); 359 360 // create the classpath entry from the paths 361 IClasspathEntry[] entries = createClasspathEntriesFromPaths(paths, target); 362 363 // paths now contains all the path required to recreate the IClasspathEntry with no 364 // target info. We encode them in a single string, with each path separated by 365 // OS path separator. 366 StringBuilder sb = new StringBuilder(CACHE_VERSION); 367 for (String p : paths) { 368 sb.append(PATH_SEPARATOR); 369 sb.append(p); 370 } 371 372 // store this in a project persistent property 373 ProjectHelper.saveStringProperty(project, PROPERTY_CONTAINER_CACHE, sb.toString()); 374 ProjectHelper.saveStringProperty(project, PROPERTY_TARGET_NAME, targetName); 375 376 return entries; 377 } 378 379 /** 380 * Generates an {@link AndroidClasspathContainer} from the project cache, if possible. 381 */ 382 private static AndroidClasspathContainer getContainerFromCache(IProject project, 383 IAndroidTarget target) { 384 // get the cached info from the project persistent properties. 385 String cache = ProjectHelper.loadStringProperty(project, PROPERTY_CONTAINER_CACHE); 386 String targetNameCache = ProjectHelper.loadStringProperty(project, PROPERTY_TARGET_NAME); 387 if (cache == null || targetNameCache == null) { 388 return null; 389 } 390 391 // the first 2 chars must match CACHE_VERSION. The 3rd char is the normal separator. 392 if (cache.startsWith(CACHE_VERSION_SEP) == false) { 393 return null; 394 } 395 396 cache = cache.substring(CACHE_VERSION_SEP.length()); 397 398 // the cache contains multiple paths, separated by a character guaranteed to not be in 399 // the path (\u001C). 400 // The first 3 are for android.jar (jar, source, doc), the rest are for the optional 401 // libraries and should contain at least one doc and a jar (if there are any libraries). 402 // Therefore, the path count should be 3 or 5+ 403 String[] paths = cache.split(Pattern.quote(PATH_SEPARATOR)); 404 if (paths.length < 3 || paths.length == 4) { 405 return null; 406 } 407 408 // now we check the paths actually exist. 409 // There's an exception: If the source folder for android.jar does not exist, this is 410 // not a problem, so we skip it. 411 // Also paths[CACHE_INDEX_DOCS_URI] is a URI to the javadoc, so we test it a 412 // bit differently. 413 try { 414 if (new File(paths[CACHE_INDEX_JAR]).exists() == false || 415 new File(new URI(paths[CACHE_INDEX_DOCS_URI])).exists() == false) { 416 return null; 417 } 418 419 // check the path for the add-ons, if they exist. 420 if (paths.length > CACHE_INDEX_ADD_ON_START) { 421 422 // check the docs path separately from the rest of the paths as it's a URI. 423 if (new File(new URI(paths[CACHE_INDEX_OPT_DOCS_URI])).exists() == false) { 424 return null; 425 } 426 427 // now just check the remaining paths. 428 for (int i = CACHE_INDEX_ADD_ON_START + 1; i < paths.length; i++) { 429 String path = paths[i]; 430 if (path.length() > 0) { 431 File f = new File(path); 432 if (f.exists() == false) { 433 return null; 434 } 435 } 436 } 437 } 438 } catch (URISyntaxException e) { 439 return null; 440 } 441 442 IClasspathEntry[] entries = createClasspathEntriesFromPaths(paths, target); 443 444 return new AndroidClasspathContainer(entries, 445 new Path(AdtConstants.CONTAINER_FRAMEWORK), 446 targetNameCache, IClasspathContainer.K_DEFAULT_SYSTEM); 447 } 448 449 /** 450 * Generates an array of {@link IClasspathEntry} from a set of paths. 451 * @see #getTargetPaths(IAndroidTarget) 452 */ 453 private static IClasspathEntry[] createClasspathEntriesFromPaths(String[] paths, 454 IAndroidTarget target) { 455 ArrayList<IClasspathEntry> list = new ArrayList<IClasspathEntry>(); 456 457 // First, we create the IClasspathEntry for the framework. 458 // now add the android framework to the class path. 459 // create the path object. 460 IPath androidLib = new Path(paths[CACHE_INDEX_JAR]); 461 462 IPath androidSrc = null; 463 String androidSrcOsPath = null; 464 IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); 465 if (target != null) { 466 androidSrcOsPath = 467 ProjectHelper.loadStringProperty(root, getAndroidSourceProperty(target)); 468 } 469 if (androidSrcOsPath != null && androidSrcOsPath.trim().length() > 0) { 470 androidSrc = new Path(androidSrcOsPath); 471 } 472 if (androidSrc == null) { 473 androidSrc = new Path(paths[CACHE_INDEX_SRC]); 474 File androidSrcFile = new File(paths[CACHE_INDEX_SRC]); 475 if (!androidSrcFile.isDirectory()) { 476 androidSrc = null; 477 } 478 } 479 480 if (androidSrc == null && target != null) { 481 Bundle bundle = getSourceBundle(); 482 483 if (bundle != null) { 484 AndroidVersion version = target.getVersion(); 485 String apiString = version.getApiString(); 486 String sourcePath = apiString + SOURCES_ZIP; 487 URL sourceURL = bundle.getEntry(sourcePath); 488 if (sourceURL != null) { 489 URL url = null; 490 try { 491 url = FileLocator.resolve(sourceURL); 492 } catch (IOException ignore) { 493 } 494 if (url != null) { 495 androidSrcOsPath = url.getFile(); 496 if (new File(androidSrcOsPath).isFile()) { 497 androidSrc = new Path(androidSrcOsPath); 498 } 499 } 500 } 501 } 502 } 503 504 // create the java doc link. 505 String androidApiURL = ProjectHelper.loadStringProperty(root, PROPERTY_ANDROID_API); 506 String apiURL = null; 507 if (androidApiURL != null && testURL(androidApiURL)) { 508 apiURL = androidApiURL; 509 } else { 510 if (testURL(paths[CACHE_INDEX_DOCS_URI])) { 511 apiURL = paths[CACHE_INDEX_DOCS_URI]; 512 } else if (testURL(ANDROID_API_REFERENCE)) { 513 apiURL = ANDROID_API_REFERENCE; 514 } 515 } 516 517 IClasspathAttribute[] attributes = null; 518 if (apiURL != null && !NULL_API_URL.equals(apiURL)) { 519 IClasspathAttribute cpAttribute = JavaCore.newClasspathAttribute( 520 IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, apiURL); 521 attributes = new IClasspathAttribute[] { 522 cpAttribute 523 }; 524 } 525 // create the access rule to restrict access to classes in 526 // com.android.internal 527 IAccessRule accessRule = JavaCore.newAccessRule(new Path("com/android/internal/**"), //$NON-NLS-1$ 528 IAccessRule.K_NON_ACCESSIBLE); 529 530 IClasspathEntry frameworkClasspathEntry = JavaCore.newLibraryEntry(androidLib, 531 androidSrc, // source attachment path 532 null, // default source attachment root path. 533 new IAccessRule[] { accessRule }, 534 attributes, 535 false // not exported. 536 ); 537 538 list.add(frameworkClasspathEntry); 539 540 // now deal with optional libraries 541 if (paths.length >= 5) { 542 String docPath = paths[CACHE_INDEX_OPT_DOCS_URI]; 543 int i = 4; 544 while (i < paths.length) { 545 Path jarPath = new Path(paths[i++]); 546 547 attributes = null; 548 if (docPath.length() > 0) { 549 attributes = new IClasspathAttribute[] { 550 JavaCore.newClasspathAttribute( 551 IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, docPath) 552 }; 553 } 554 555 IClasspathEntry entry = JavaCore.newLibraryEntry( 556 jarPath, 557 null, // source attachment path 558 null, // default source attachment root path. 559 null, 560 attributes, 561 false // not exported. 562 ); 563 list.add(entry); 564 } 565 } 566 567 if (apiURL != null) { 568 ProjectHelper.saveStringProperty(root, PROPERTY_ANDROID_API, apiURL); 569 } 570 if (androidSrc != null && target != null) { 571 ProjectHelper.saveStringProperty(root, getAndroidSourceProperty(target), 572 androidSrc.toOSString()); 573 } 574 return list.toArray(new IClasspathEntry[list.size()]); 575 } 576 577 private static Bundle getSourceBundle() { 578 String bundleId = System.getProperty(COM_ANDROID_IDE_ECLIPSE_ADT_SOURCE, 579 COM_ANDROID_IDE_ECLIPSE_ADT_SOURCE); 580 Bundle bundle = Platform.getBundle(bundleId); 581 return bundle; 582 } 583 584 private static String getAndroidSourceProperty(IAndroidTarget target) { 585 if (target == null) { 586 return null; 587 } 588 String androidSourceProperty = PROPERTY_ANDROID_SOURCE + "_" 589 + target.getVersion().getApiString(); 590 return androidSourceProperty; 591 } 592 593 private static boolean testURL(String androidApiURL) { 594 boolean valid = false; 595 InputStream is = null; 596 try { 597 URL testURL = new URL(androidApiURL); 598 is = testURL.openStream(); 599 valid = true; 600 } catch (Exception ignore) { 601 } finally { 602 if (is != null) { 603 try { 604 is.close(); 605 } catch (IOException ignore) { 606 } 607 } 608 } 609 return valid; 610 } 611 612 /** 613 * Checks the projects' caches. If the cache was valid, the project is removed from the list. 614 * @param projects the list of projects to check. 615 */ 616 public static void checkProjectsCache(ArrayList<IJavaProject> projects) { 617 Sdk currentSdk = Sdk.getCurrent(); 618 int i = 0; 619 projectLoop: while (i < projects.size()) { 620 IJavaProject javaProject = projects.get(i); 621 IProject iProject = javaProject.getProject(); 622 623 // check if the project is opened 624 if (iProject.isOpen() == false) { 625 // remove from the list 626 // we do not increment i in this case. 627 projects.remove(i); 628 629 continue; 630 } 631 632 // project that have been resolved before the sdk was loaded 633 // will have a ProjectState where the IAndroidTarget is null 634 // so we load the target now that the SDK is loaded. 635 IAndroidTarget target = currentSdk.loadTarget(Sdk.getProjectState(iProject)); 636 if (target == null) { 637 // this is really not supposed to happen. This would mean there are cached paths, 638 // but project.properties was deleted. Keep the project in the list to force 639 // a resolve which will display the error. 640 i++; 641 continue; 642 } 643 644 String[] targetPaths = getTargetPaths(target); 645 646 // now get the cached paths 647 String cache = ProjectHelper.loadStringProperty(iProject, PROPERTY_CONTAINER_CACHE); 648 if (cache == null) { 649 // this should not happen. We'll force resolve again anyway. 650 i++; 651 continue; 652 } 653 654 String[] cachedPaths = cache.split(Pattern.quote(PATH_SEPARATOR)); 655 if (cachedPaths.length < 3 || cachedPaths.length == 4) { 656 // paths length is wrong. simply resolve the project again 657 i++; 658 continue; 659 } 660 661 // Now we compare the paths. The first 4 can be compared directly. 662 // because of case sensitiveness we need to use File objects 663 664 if (targetPaths.length != cachedPaths.length) { 665 // different paths, force resolve again. 666 i++; 667 continue; 668 } 669 670 // compare the main paths (android.jar, main sources, main javadoc) 671 if (new File(targetPaths[CACHE_INDEX_JAR]).equals( 672 new File(cachedPaths[CACHE_INDEX_JAR])) == false || 673 new File(targetPaths[CACHE_INDEX_SRC]).equals( 674 new File(cachedPaths[CACHE_INDEX_SRC])) == false || 675 new File(targetPaths[CACHE_INDEX_DOCS_URI]).equals( 676 new File(cachedPaths[CACHE_INDEX_DOCS_URI])) == false) { 677 // different paths, force resolve again. 678 i++; 679 continue; 680 } 681 682 if (cachedPaths.length > CACHE_INDEX_OPT_DOCS_URI) { 683 // compare optional libraries javadoc 684 if (new File(targetPaths[CACHE_INDEX_OPT_DOCS_URI]).equals( 685 new File(cachedPaths[CACHE_INDEX_OPT_DOCS_URI])) == false) { 686 // different paths, force resolve again. 687 i++; 688 continue; 689 } 690 691 // testing the optional jar files is a little bit trickier. 692 // The order is not guaranteed to be identical. 693 // From a previous test, we do know however that there is the same number. 694 // The number of libraries should be low enough that we can simply go through the 695 // lists manually. 696 targetLoop: for (int tpi = 4 ; tpi < targetPaths.length; tpi++) { 697 String targetPath = targetPaths[tpi]; 698 699 // look for a match in the other array 700 for (int cpi = 4 ; cpi < cachedPaths.length; cpi++) { 701 if (new File(targetPath).equals(new File(cachedPaths[cpi]))) { 702 // found a match. Try the next targetPath 703 continue targetLoop; 704 } 705 } 706 707 // if we stop here, we haven't found a match, which means there's a 708 // discrepancy in the libraries. We force a resolve. 709 i++; 710 continue projectLoop; 711 } 712 } 713 714 // at the point the check passes, and we can remove the project from the list. 715 // we do not increment i in this case. 716 projects.remove(i); 717 } 718 } 719 720 /** 721 * Returns the paths necessary to create the {@link IClasspathEntry} for this targets. 722 * <p/>The paths are always in the same order. 723 * <ul> 724 * <li>Path to android.jar</li> 725 * <li>Path to the source code for android.jar</li> 726 * <li>Path to the javadoc for the android platform</li> 727 * </ul> 728 * Additionally, if there are optional libraries, the array will contain: 729 * <ul> 730 * <li>Path to the libraries javadoc</li> 731 * <li>Path to the first .jar file</li> 732 * <li>(more .jar as needed)</li> 733 * </ul> 734 */ 735 private static String[] getTargetPaths(IAndroidTarget target) { 736 ArrayList<String> paths = new ArrayList<String>(); 737 738 // first, we get the path for android.jar 739 // The order is: android.jar, source folder, docs folder 740 paths.add(target.getPath(IAndroidTarget.ANDROID_JAR)); 741 paths.add(target.getPath(IAndroidTarget.SOURCES)); 742 paths.add(AdtPlugin.getUrlDoc()); 743 744 // now deal with optional libraries. 745 IOptionalLibrary[] libraries = target.getOptionalLibraries(); 746 if (libraries != null) { 747 // all the optional libraries use the same javadoc, so we start with this 748 String targetDocPath = target.getPath(IAndroidTarget.DOCS); 749 if (targetDocPath != null) { 750 paths.add(ProjectHelper.getJavaDocPath(targetDocPath)); 751 } else { 752 // we add an empty string, to always have the same count. 753 paths.add(""); 754 } 755 756 // because different libraries could use the same jar file, we make sure we add 757 // each jar file only once. 758 HashSet<String> visitedJars = new HashSet<String>(); 759 for (IOptionalLibrary library : libraries) { 760 String jarPath = library.getJarPath(); 761 if (visitedJars.contains(jarPath) == false) { 762 visitedJars.add(jarPath); 763 paths.add(jarPath); 764 } 765 } 766 } 767 768 return paths.toArray(new String[paths.size()]); 769 } 770 771 @Override 772 public boolean canUpdateClasspathContainer(IPath containerPath, IJavaProject project) { 773 return true; 774 } 775 776 @Override 777 public void requestClasspathContainerUpdate(IPath containerPath, IJavaProject project, 778 IClasspathContainer containerSuggestion) throws CoreException { 779 AdtPlugin plugin = AdtPlugin.getDefault(); 780 781 synchronized (Sdk.getLock()) { 782 boolean sdkIsLoaded = plugin.getSdkLoadStatus() == LoadStatus.LOADED; 783 784 // check if the project has a valid target. 785 IAndroidTarget target = null; 786 if (sdkIsLoaded) { 787 target = Sdk.getCurrent().getTarget(project.getProject()); 788 } 789 if (sdkIsLoaded && target != null) { 790 String[] paths = getTargetPaths(target); 791 IPath android_lib = new Path(paths[CACHE_INDEX_JAR]); 792 IClasspathEntry[] entries = containerSuggestion.getClasspathEntries(); 793 for (int i = 0; i < entries.length; i++) { 794 IClasspathEntry entry = entries[i]; 795 if (entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) { 796 IPath entryPath = entry.getPath(); 797 798 if (entryPath != null) { 799 if (entryPath.equals(android_lib)) { 800 IPath entrySrcPath = entry.getSourceAttachmentPath(); 801 IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); 802 if (entrySrcPath != null) { 803 ProjectHelper.saveStringProperty(root, 804 getAndroidSourceProperty(target), 805 entrySrcPath.toString()); 806 } else { 807 ProjectHelper.saveStringProperty(root, 808 getAndroidSourceProperty(target), null); 809 } 810 IClasspathAttribute[] extraAttributtes = entry.getExtraAttributes(); 811 if (extraAttributtes.length == 0) { 812 ProjectHelper.saveStringProperty(root, PROPERTY_ANDROID_API, 813 NULL_API_URL); 814 } 815 for (int j = 0; j < extraAttributtes.length; j++) { 816 IClasspathAttribute extraAttribute = extraAttributtes[j]; 817 String value = extraAttribute.getValue(); 818 if ((value == null || value.trim().length() == 0) 819 && IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME 820 .equals(extraAttribute.getName())) { 821 value = NULL_API_URL; 822 } 823 if (IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME 824 .equals(extraAttribute.getName())) { 825 ProjectHelper.saveStringProperty(root, 826 PROPERTY_ANDROID_API, value); 827 828 } 829 } 830 } 831 } 832 } 833 } 834 rebindClasspathEntries(project.getJavaModel(), containerPath); 835 } 836 } 837 } 838 839 private static void rebindClasspathEntries(IJavaModel model, IPath containerPath) 840 throws JavaModelException { 841 ArrayList<IJavaProject> affectedProjects = new ArrayList<IJavaProject>(); 842 843 IJavaProject[] projects = model.getJavaProjects(); 844 for (int i = 0; i < projects.length; i++) { 845 IJavaProject project = projects[i]; 846 IClasspathEntry[] entries = project.getRawClasspath(); 847 for (int k = 0; k < entries.length; k++) { 848 IClasspathEntry curr = entries[k]; 849 if (curr.getEntryKind() == IClasspathEntry.CPE_CONTAINER 850 && containerPath.equals(curr.getPath())) { 851 affectedProjects.add(project); 852 } 853 } 854 } 855 if (!affectedProjects.isEmpty()) { 856 IJavaProject[] affected = affectedProjects 857 .toArray(new IJavaProject[affectedProjects.size()]); 858 updateProjects(affected); 859 } 860 } 861 862 } 863