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.eclipse.adt.AdtConstants; 20 import com.android.ide.eclipse.adt.AdtPlugin; 21 import com.android.ide.eclipse.adt.internal.sdk.LoadStatus; 22 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 23 import com.android.sdklib.IAndroidTarget; 24 import com.android.sdklib.IAndroidTarget.IOptionalLibrary; 25 26 import org.eclipse.core.resources.IMarker; 27 import org.eclipse.core.resources.IProject; 28 import org.eclipse.core.resources.IResource; 29 import org.eclipse.core.runtime.CoreException; 30 import org.eclipse.core.runtime.IPath; 31 import org.eclipse.core.runtime.IProgressMonitor; 32 import org.eclipse.core.runtime.IStatus; 33 import org.eclipse.core.runtime.NullProgressMonitor; 34 import org.eclipse.core.runtime.Path; 35 import org.eclipse.core.runtime.Status; 36 import org.eclipse.core.runtime.jobs.Job; 37 import org.eclipse.jdt.core.ClasspathContainerInitializer; 38 import org.eclipse.jdt.core.IAccessRule; 39 import org.eclipse.jdt.core.IClasspathAttribute; 40 import org.eclipse.jdt.core.IClasspathContainer; 41 import org.eclipse.jdt.core.IClasspathEntry; 42 import org.eclipse.jdt.core.IJavaProject; 43 import org.eclipse.jdt.core.JavaCore; 44 import org.eclipse.jdt.core.JavaModelException; 45 46 import java.io.File; 47 import java.net.URI; 48 import java.net.URISyntaxException; 49 import java.util.ArrayList; 50 import java.util.HashSet; 51 import java.util.regex.Pattern; 52 53 /** 54 * Classpath container initializer responsible for binding {@link AndroidClasspathContainer} to 55 * {@link IProject}s. This removes the hard-coded path to the android.jar. 56 */ 57 public class AndroidClasspathContainerInitializer extends ClasspathContainerInitializer { 58 /** The container id for the android framework jar file */ 59 private final static String CONTAINER_ID = 60 "com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"; //$NON-NLS-1$ 61 62 /** path separator to store multiple paths in a single property. This is guaranteed to not 63 * be in a path. 64 */ 65 private final static String PATH_SEPARATOR = "\u001C"; //$NON-NLS-1$ 66 67 private final static String PROPERTY_CONTAINER_CACHE = "androidContainerCache"; //$NON-NLS-1$ 68 private final static String PROPERTY_TARGET_NAME = "androidTargetCache"; //$NON-NLS-1$ 69 private final static String CACHE_VERSION = "01"; //$NON-NLS-1$ 70 private final static String CACHE_VERSION_SEP = CACHE_VERSION + PATH_SEPARATOR; 71 72 private final static int CACHE_INDEX_JAR = 0; 73 private final static int CACHE_INDEX_SRC = 1; 74 private final static int CACHE_INDEX_DOCS_URI = 2; 75 private final static int CACHE_INDEX_OPT_DOCS_URI = 3; 76 private final static int CACHE_INDEX_ADD_ON_START = CACHE_INDEX_OPT_DOCS_URI; 77 AndroidClasspathContainerInitializer()78 public AndroidClasspathContainerInitializer() { 79 // pass 80 } 81 82 /** 83 * Binds a classpath container to a {@link IClasspathContainer} for a given project, 84 * or silently fails if unable to do so. 85 * @param containerPath the container path that is the container id. 86 * @param project the project to bind 87 */ 88 @Override initialize(IPath containerPath, IJavaProject project)89 public void initialize(IPath containerPath, IJavaProject project) throws CoreException { 90 if (CONTAINER_ID.equals(containerPath.toString())) { 91 JavaCore.setClasspathContainer(new Path(CONTAINER_ID), 92 new IJavaProject[] { project }, 93 new IClasspathContainer[] { allocateAndroidContainer(project) }, 94 new NullProgressMonitor()); 95 } 96 } 97 98 /** 99 * Creates a new {@link IClasspathEntry} of type {@link IClasspathEntry#CPE_CONTAINER} 100 * linking to the Android Framework. 101 */ getContainerEntry()102 public static IClasspathEntry getContainerEntry() { 103 return JavaCore.newContainerEntry(new Path(CONTAINER_ID)); 104 } 105 106 /** 107 * Checks the {@link IPath} objects against the android framework container id and 108 * returns <code>true</code> if they are identical. 109 * @param path the <code>IPath</code> to check. 110 */ checkPath(IPath path)111 public static boolean checkPath(IPath path) { 112 return CONTAINER_ID.equals(path.toString()); 113 } 114 115 /** 116 * Updates the {@link IJavaProject} objects with new android framework container. This forces 117 * JDT to recompile them. 118 * @param androidProjects the projects to update. 119 * @return <code>true</code> if success, <code>false</code> otherwise. 120 */ updateProjects(IJavaProject[] androidProjects)121 public static boolean updateProjects(IJavaProject[] androidProjects) { 122 try { 123 // Allocate a new AndroidClasspathContainer, and associate it to the android framework 124 // container id for each projects. 125 // By providing a new association between a container id and a IClasspathContainer, 126 // this forces the JDT to query the IClasspathContainer for new IClasspathEntry (with 127 // IClasspathContainer#getClasspathEntries()), and therefore force recompilation of 128 // the projects. 129 int projectCount = androidProjects.length; 130 131 IClasspathContainer[] containers = new IClasspathContainer[projectCount]; 132 for (int i = 0 ; i < projectCount; i++) { 133 containers[i] = allocateAndroidContainer(androidProjects[i]); 134 } 135 136 // give each project their new container in one call. 137 JavaCore.setClasspathContainer( 138 new Path(CONTAINER_ID), 139 androidProjects, containers, new NullProgressMonitor()); 140 141 return true; 142 } catch (JavaModelException e) { 143 return false; 144 } 145 } 146 147 /** 148 * Allocates and returns an {@link AndroidClasspathContainer} object with the proper 149 * path to the framework jar file. 150 * @param javaProject The java project that will receive the container. 151 */ allocateAndroidContainer(IJavaProject javaProject)152 private static IClasspathContainer allocateAndroidContainer(IJavaProject javaProject) { 153 final IProject iProject = javaProject.getProject(); 154 155 String markerMessage = null; 156 boolean outputToConsole = true; 157 158 try { 159 AdtPlugin plugin = AdtPlugin.getDefault(); 160 161 // get the lock object for project manipulation during SDK load. 162 Object lock = plugin.getSdkLockObject(); 163 synchronized (lock) { 164 boolean sdkIsLoaded = plugin.getSdkLoadStatus() == LoadStatus.LOADED; 165 166 // check if the project has a valid target. 167 IAndroidTarget target = null; 168 if (sdkIsLoaded) { 169 target = Sdk.getCurrent().getTarget(iProject); 170 } 171 172 // if we are loaded and the target is non null, we create a valid ClassPathContainer 173 if (sdkIsLoaded && target != null) { 174 175 String targetName = target.getClasspathName(); 176 177 return new AndroidClasspathContainer( 178 createClasspathEntries(iProject, target, targetName), 179 new Path(CONTAINER_ID), targetName); 180 } 181 182 // In case of error, we'll try different thing to provide the best error message 183 // possible. 184 // Get the project's target's hash string (if it exists) 185 String hashString = Sdk.getProjectTargetHashString(iProject); 186 187 if (hashString == null || hashString.length() == 0) { 188 // if there is no hash string we only show this if the SDK is loaded. 189 // For a project opened at start-up with no target, this would be displayed 190 // twice, once when the project is opened, and once after the SDK has 191 // finished loading. 192 // By testing the sdk is loaded, we only show this once in the console. 193 if (sdkIsLoaded) { 194 markerMessage = String.format( 195 "Project has no target set. Edit the project properties to set one."); 196 } 197 } else if (sdkIsLoaded) { 198 markerMessage = String.format( 199 "Unable to resolve target '%s'", hashString); 200 } else { 201 // this is the case where there is a hashString but the SDK is not yet 202 // loaded and therefore we can't get the target yet. 203 // We check if there is a cache of the needed information. 204 AndroidClasspathContainer container = getContainerFromCache(iProject); 205 206 if (container == null) { 207 // either the cache was wrong (ie folder does not exists anymore), or 208 // there was no cache. In this case we need to make sure the project 209 // is resolved again after the SDK is loaded. 210 plugin.setProjectToResolve(javaProject); 211 212 markerMessage = String.format( 213 "Unable to resolve target '%s' until the SDK is loaded.", 214 hashString); 215 216 // let's not log this one to the console as it will happen at every boot, 217 // and it's expected. (we do keep the error marker though). 218 outputToConsole = false; 219 220 } else { 221 // we created a container from the cache, so we register the project 222 // to be checked for cache validity once the SDK is loaded 223 plugin.setProjectToCheck(javaProject); 224 225 // and return the container 226 return container; 227 } 228 229 } 230 231 // return a dummy container to replace the one we may have had before. 232 // It'll be replaced by the real when if/when the target is resolved if/when the 233 // SDK finishes loading. 234 return new IClasspathContainer() { 235 public IClasspathEntry[] getClasspathEntries() { 236 return new IClasspathEntry[0]; 237 } 238 239 public String getDescription() { 240 return "Unable to get system library for the project"; 241 } 242 243 public int getKind() { 244 return IClasspathContainer.K_DEFAULT_SYSTEM; 245 } 246 247 public IPath getPath() { 248 return null; 249 } 250 }; 251 } 252 } finally { 253 if (markerMessage != null) { 254 // log the error and put the marker on the project if we can. 255 if (outputToConsole) { 256 AdtPlugin.printErrorToConsole(iProject, markerMessage); 257 } 258 259 try { 260 BaseProjectHelper.addMarker(iProject, AdtConstants.MARKER_TARGET, markerMessage, 261 -1, IMarker.SEVERITY_ERROR, IMarker.PRIORITY_HIGH); 262 } catch (CoreException e) { 263 // In some cases, the workspace may be locked for modification when we 264 // pass here. 265 // We schedule a new job to put the marker after. 266 final String fmessage = markerMessage; 267 Job markerJob = new Job("Android SDK: Resolving error markers") { 268 @Override 269 protected IStatus run(IProgressMonitor monitor) { 270 try { 271 BaseProjectHelper.addMarker(iProject, AdtConstants.MARKER_TARGET, 272 fmessage, -1, IMarker.SEVERITY_ERROR, 273 IMarker.PRIORITY_HIGH); 274 } catch (CoreException e2) { 275 return e2.getStatus(); 276 } 277 278 return Status.OK_STATUS; 279 } 280 }; 281 282 // build jobs are run after other interactive jobs 283 markerJob.setPriority(Job.BUILD); 284 markerJob.schedule(); 285 } 286 } else { 287 // no error, remove potential MARKER_TARGETs. 288 try { 289 if (iProject.exists()) { 290 iProject.deleteMarkers(AdtConstants.MARKER_TARGET, true, 291 IResource.DEPTH_INFINITE); 292 } 293 } catch (CoreException ce) { 294 // In some cases, the workspace may be locked for modification when we pass 295 // here, so we schedule a new job to put the marker after. 296 Job markerJob = new Job("Android SDK: Resolving error markers") { 297 @Override 298 protected IStatus run(IProgressMonitor monitor) { 299 try { 300 iProject.deleteMarkers(AdtConstants.MARKER_TARGET, true, 301 IResource.DEPTH_INFINITE); 302 } catch (CoreException e2) { 303 return e2.getStatus(); 304 } 305 306 return Status.OK_STATUS; 307 } 308 }; 309 310 // build jobs are run after other interactive jobs 311 markerJob.setPriority(Job.BUILD); 312 markerJob.schedule(); 313 } 314 } 315 } 316 } 317 318 /** 319 * Creates and returns an array of {@link IClasspathEntry} objects for the android 320 * framework and optional libraries. 321 * <p/>This references the OS path to the android.jar and the 322 * java doc directory. This is dynamically created when a project is opened, 323 * and never saved in the project itself, so there's no risk of storing an 324 * obsolete path. 325 * The method also stores the paths used to create the entries in the project persistent 326 * properties. A new {@link AndroidClasspathContainer} can be created from the stored path 327 * using the {@link #getContainerFromCache(IProject)} method. 328 * @param project 329 * @param target The target that contains the libraries. 330 * @param targetName 331 */ 332 private static IClasspathEntry[] createClasspathEntries(IProject project, 333 IAndroidTarget target, String targetName) { 334 335 // get the path from the target 336 String[] paths = getTargetPaths(target); 337 338 // create the classpath entry from the paths 339 IClasspathEntry[] entries = createClasspathEntriesFromPaths(paths); 340 341 // paths now contains all the path required to recreate the IClasspathEntry with no 342 // target info. We encode them in a single string, with each path separated by 343 // OS path separator. 344 StringBuilder sb = new StringBuilder(CACHE_VERSION); 345 for (String p : paths) { 346 sb.append(PATH_SEPARATOR); 347 sb.append(p); 348 } 349 350 // store this in a project persistent property 351 ProjectHelper.saveStringProperty(project, PROPERTY_CONTAINER_CACHE, sb.toString()); 352 ProjectHelper.saveStringProperty(project, PROPERTY_TARGET_NAME, targetName); 353 354 return entries; 355 } 356 357 /** 358 * Generates an {@link AndroidClasspathContainer} from the project cache, if possible. 359 */ 360 private static AndroidClasspathContainer getContainerFromCache(IProject project) { 361 // get the cached info from the project persistent properties. 362 String cache = ProjectHelper.loadStringProperty(project, PROPERTY_CONTAINER_CACHE); 363 String targetNameCache = ProjectHelper.loadStringProperty(project, PROPERTY_TARGET_NAME); 364 if (cache == null || targetNameCache == null) { 365 return null; 366 } 367 368 // the first 2 chars must match CACHE_VERSION. The 3rd char is the normal separator. 369 if (cache.startsWith(CACHE_VERSION_SEP) == false) { 370 return null; 371 } 372 373 cache = cache.substring(CACHE_VERSION_SEP.length()); 374 375 // the cache contains multiple paths, separated by a character guaranteed to not be in 376 // the path (\u001C). 377 // The first 3 are for android.jar (jar, source, doc), the rest are for the optional 378 // libraries and should contain at least one doc and a jar (if there are any libraries). 379 // Therefore, the path count should be 3 or 5+ 380 String[] paths = cache.split(Pattern.quote(PATH_SEPARATOR)); 381 if (paths.length < 3 || paths.length == 4) { 382 return null; 383 } 384 385 // now we check the paths actually exist. 386 // There's an exception: If the source folder for android.jar does not exist, this is 387 // not a problem, so we skip it. 388 // Also paths[CACHE_INDEX_DOCS_URI] is a URI to the javadoc, so we test it a 389 // bit differently. 390 try { 391 if (new File(paths[CACHE_INDEX_JAR]).exists() == false || 392 new File(new URI(paths[CACHE_INDEX_DOCS_URI])).exists() == false) { 393 return null; 394 } 395 396 // check the path for the add-ons, if they exist. 397 if (paths.length > CACHE_INDEX_ADD_ON_START) { 398 399 // check the docs path separately from the rest of the paths as it's a URI. 400 if (new File(new URI(paths[CACHE_INDEX_OPT_DOCS_URI])).exists() == false) { 401 return null; 402 } 403 404 // now just check the remaining paths. 405 for (int i = CACHE_INDEX_ADD_ON_START + 1; i < paths.length; i++) { 406 String path = paths[i]; 407 if (path.length() > 0) { 408 File f = new File(path); 409 if (f.exists() == false) { 410 return null; 411 } 412 } 413 } 414 } 415 } catch (URISyntaxException e) { 416 return null; 417 } 418 419 IClasspathEntry[] entries = createClasspathEntriesFromPaths(paths); 420 421 return new AndroidClasspathContainer(entries, 422 new Path(CONTAINER_ID), targetNameCache); 423 } 424 425 /** 426 * Generates an array of {@link IClasspathEntry} from a set of paths. 427 * @see #getTargetPaths(IAndroidTarget) 428 */ 429 private static IClasspathEntry[] createClasspathEntriesFromPaths(String[] paths) { 430 ArrayList<IClasspathEntry> list = new ArrayList<IClasspathEntry>(); 431 432 // First, we create the IClasspathEntry for the framework. 433 // now add the android framework to the class path. 434 // create the path object. 435 IPath android_lib = new Path(paths[CACHE_INDEX_JAR]); 436 IPath android_src = new Path(paths[CACHE_INDEX_SRC]); 437 438 // create the java doc link. 439 IClasspathAttribute cpAttribute = JavaCore.newClasspathAttribute( 440 IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, 441 paths[CACHE_INDEX_DOCS_URI]); 442 443 // create the access rule to restrict access to classes in com.android.internal 444 IAccessRule accessRule = JavaCore.newAccessRule( 445 new Path("com/android/internal/**"), //$NON-NLS-1$ 446 IAccessRule.K_NON_ACCESSIBLE); 447 448 IClasspathEntry frameworkClasspathEntry = JavaCore.newLibraryEntry(android_lib, 449 android_src, // source attachment path 450 null, // default source attachment root path. 451 new IAccessRule[] { accessRule }, 452 new IClasspathAttribute[] { cpAttribute }, 453 false // not exported. 454 ); 455 456 list.add(frameworkClasspathEntry); 457 458 // now deal with optional libraries 459 if (paths.length >= 5) { 460 String docPath = paths[CACHE_INDEX_OPT_DOCS_URI]; 461 int i = 4; 462 while (i < paths.length) { 463 Path jarPath = new Path(paths[i++]); 464 465 IClasspathAttribute[] attributes = null; 466 if (docPath.length() > 0) { 467 attributes = new IClasspathAttribute[] { 468 JavaCore.newClasspathAttribute( 469 IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, 470 docPath) 471 }; 472 } 473 474 IClasspathEntry entry = JavaCore.newLibraryEntry( 475 jarPath, 476 null, // source attachment path 477 null, // default source attachment root path. 478 null, 479 attributes, 480 false // not exported. 481 ); 482 list.add(entry); 483 } 484 } 485 486 return list.toArray(new IClasspathEntry[list.size()]); 487 } 488 489 /** 490 * Checks the projects' caches. If the cache was valid, the project is removed from the list. 491 * @param projects the list of projects to check. 492 */ 493 public static void checkProjectsCache(ArrayList<IJavaProject> projects) { 494 int i = 0; 495 projectLoop: while (i < projects.size()) { 496 IJavaProject javaProject = projects.get(i); 497 IProject iProject = javaProject.getProject(); 498 499 // check if the project is opened 500 if (iProject.isOpen() == false) { 501 // remove from the list 502 // we do not increment i in this case. 503 projects.remove(i); 504 505 continue; 506 } 507 508 // get the target from the project and its paths 509 IAndroidTarget target = Sdk.getCurrent().getTarget(javaProject.getProject()); 510 if (target == null) { 511 // this is really not supposed to happen. This would mean there are cached paths, 512 // but default.properties was deleted. Keep the project in the list to force 513 // a resolve which will display the error. 514 i++; 515 continue; 516 } 517 518 String[] targetPaths = getTargetPaths(target); 519 520 // now get the cached paths 521 String cache = ProjectHelper.loadStringProperty(iProject, PROPERTY_CONTAINER_CACHE); 522 if (cache == null) { 523 // this should not happen. We'll force resolve again anyway. 524 i++; 525 continue; 526 } 527 528 String[] cachedPaths = cache.split(Pattern.quote(PATH_SEPARATOR)); 529 if (cachedPaths.length < 3 || cachedPaths.length == 4) { 530 // paths length is wrong. simply resolve the project again 531 i++; 532 continue; 533 } 534 535 // Now we compare the paths. The first 4 can be compared directly. 536 // because of case sensitiveness we need to use File objects 537 538 if (targetPaths.length != cachedPaths.length) { 539 // different paths, force resolve again. 540 i++; 541 continue; 542 } 543 544 // compare the main paths (android.jar, main sources, main javadoc) 545 if (new File(targetPaths[CACHE_INDEX_JAR]).equals( 546 new File(cachedPaths[CACHE_INDEX_JAR])) == false || 547 new File(targetPaths[CACHE_INDEX_SRC]).equals( 548 new File(cachedPaths[CACHE_INDEX_SRC])) == false || 549 new File(targetPaths[CACHE_INDEX_DOCS_URI]).equals( 550 new File(cachedPaths[CACHE_INDEX_DOCS_URI])) == false) { 551 // different paths, force resolve again. 552 i++; 553 continue; 554 } 555 556 if (cachedPaths.length > CACHE_INDEX_OPT_DOCS_URI) { 557 // compare optional libraries javadoc 558 if (new File(targetPaths[CACHE_INDEX_OPT_DOCS_URI]).equals( 559 new File(cachedPaths[CACHE_INDEX_OPT_DOCS_URI])) == false) { 560 // different paths, force resolve again. 561 i++; 562 continue; 563 } 564 565 // testing the optional jar files is a little bit trickier. 566 // The order is not guaranteed to be identical. 567 // From a previous test, we do know however that there is the same number. 568 // The number of libraries should be low enough that we can simply go through the 569 // lists manually. 570 targetLoop: for (int tpi = 4 ; tpi < targetPaths.length; tpi++) { 571 String targetPath = targetPaths[tpi]; 572 573 // look for a match in the other array 574 for (int cpi = 4 ; cpi < cachedPaths.length; cpi++) { 575 if (new File(targetPath).equals(new File(cachedPaths[cpi]))) { 576 // found a match. Try the next targetPath 577 continue targetLoop; 578 } 579 } 580 581 // if we stop here, we haven't found a match, which means there's a 582 // discrepancy in the libraries. We force a resolve. 583 i++; 584 continue projectLoop; 585 } 586 } 587 588 // at the point the check passes, and we can remove the project from the list. 589 // we do not increment i in this case. 590 projects.remove(i); 591 } 592 } 593 594 /** 595 * Returns the paths necessary to create the {@link IClasspathEntry} for this targets. 596 * <p/>The paths are always in the same order. 597 * <ul> 598 * <li>Path to android.jar</li> 599 * <li>Path to the source code for android.jar</li> 600 * <li>Path to the javadoc for the android platform</li> 601 * </ul> 602 * Additionally, if there are optional libraries, the array will contain: 603 * <ul> 604 * <li>Path to the librairies javadoc</li> 605 * <li>Path to the first .jar file</li> 606 * <li>(more .jar as needed)</li> 607 * </ul> 608 */ 609 private static String[] getTargetPaths(IAndroidTarget target) { 610 ArrayList<String> paths = new ArrayList<String>(); 611 612 // first, we get the path for android.jar 613 // The order is: android.jar, source folder, docs folder 614 paths.add(target.getPath(IAndroidTarget.ANDROID_JAR)); 615 paths.add(target.getPath(IAndroidTarget.SOURCES)); 616 paths.add(AdtPlugin.getUrlDoc()); 617 618 // now deal with optional libraries. 619 IOptionalLibrary[] libraries = target.getOptionalLibraries(); 620 if (libraries != null) { 621 // all the optional libraries use the same javadoc, so we start with this 622 String targetDocPath = target.getPath(IAndroidTarget.DOCS); 623 if (targetDocPath != null) { 624 paths.add(ProjectHelper.getJavaDocPath(targetDocPath)); 625 } else { 626 // we add an empty string, to always have the same count. 627 paths.add(""); 628 } 629 630 // because different libraries could use the same jar file, we make sure we add 631 // each jar file only once. 632 HashSet<String> visitedJars = new HashSet<String>(); 633 for (IOptionalLibrary library : libraries) { 634 String jarPath = library.getJarPath(); 635 if (visitedJars.contains(jarPath) == false) { 636 visitedJars.add(jarPath); 637 paths.add(jarPath); 638 } 639 } 640 } 641 642 return paths.toArray(new String[paths.size()]); 643 } 644 } 645