1 /* 2 * Copyright (C) 2011 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 static com.android.ide.eclipse.adt.AdtConstants.CONTAINER_DEPENDENCIES; 20 21 import com.android.SdkConstants; 22 import com.android.ide.eclipse.adt.AdtConstants; 23 import com.android.ide.eclipse.adt.AdtPlugin; 24 import com.android.ide.eclipse.adt.AndroidPrintStream; 25 import com.android.ide.eclipse.adt.internal.sdk.ProjectState; 26 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 27 import com.android.sdklib.build.JarListSanitizer; 28 import com.android.sdklib.build.JarListSanitizer.DifferentLibException; 29 import com.android.sdklib.build.JarListSanitizer.Sha1Exception; 30 31 import org.eclipse.core.resources.IFile; 32 import org.eclipse.core.resources.IFolder; 33 import org.eclipse.core.resources.IProject; 34 import org.eclipse.core.resources.IResource; 35 import org.eclipse.core.resources.IWorkspaceRoot; 36 import org.eclipse.core.resources.ResourcesPlugin; 37 import org.eclipse.core.runtime.CoreException; 38 import org.eclipse.core.runtime.IPath; 39 import org.eclipse.core.runtime.NullProgressMonitor; 40 import org.eclipse.core.runtime.Path; 41 import org.eclipse.jdt.core.IAccessRule; 42 import org.eclipse.jdt.core.IClasspathAttribute; 43 import org.eclipse.jdt.core.IClasspathContainer; 44 import org.eclipse.jdt.core.IClasspathEntry; 45 import org.eclipse.jdt.core.IJavaProject; 46 import org.eclipse.jdt.core.JavaCore; 47 import org.eclipse.jdt.core.JavaModelException; 48 49 import java.io.File; 50 import java.io.FileInputStream; 51 import java.io.FileNotFoundException; 52 import java.io.IOException; 53 import java.io.InputStream; 54 import java.net.MalformedURLException; 55 import java.util.ArrayList; 56 import java.util.HashSet; 57 import java.util.List; 58 import java.util.Properties; 59 import java.util.Set; 60 61 public class LibraryClasspathContainerInitializer extends BaseClasspathContainerInitializer { 62 63 private final static String ATTR_SRC = "src"; //$NON-NLS-1$ 64 private final static String ATTR_DOC = "doc"; //$NON-NLS-1$ 65 private final static String DOT_PROPERTIES = ".properties"; //$NON-NLS-1$ 66 67 public LibraryClasspathContainerInitializer() { 68 } 69 70 /** 71 * Updates the {@link IJavaProject} objects with new library. 72 * @param androidProjects the projects to update. 73 * @return <code>true</code> if success, <code>false</code> otherwise. 74 */ 75 public static boolean updateProjects(IJavaProject[] androidProjects) { 76 try { 77 // Allocate a new AndroidClasspathContainer, and associate it to the library 78 // container id for each projects. 79 int projectCount = androidProjects.length; 80 81 IClasspathContainer[] libraryContainers = new IClasspathContainer[projectCount]; 82 IClasspathContainer[] dependencyContainers = new IClasspathContainer[projectCount]; 83 for (int i = 0 ; i < projectCount; i++) { 84 libraryContainers[i] = allocateLibraryContainer(androidProjects[i]); 85 dependencyContainers[i] = allocateDependencyContainer(androidProjects[i]); 86 } 87 88 // give each project their new container in one call. 89 JavaCore.setClasspathContainer( 90 new Path(AdtConstants.CONTAINER_PRIVATE_LIBRARIES), 91 androidProjects, libraryContainers, new NullProgressMonitor()); 92 93 JavaCore.setClasspathContainer( 94 new Path(AdtConstants.CONTAINER_DEPENDENCIES), 95 androidProjects, dependencyContainers, new NullProgressMonitor()); 96 return true; 97 } catch (JavaModelException e) { 98 return false; 99 } 100 } 101 102 /** 103 * Updates the {@link IJavaProject} objects with new library. 104 * @param androidProjects the projects to update. 105 * @return <code>true</code> if success, <code>false</code> otherwise. 106 */ 107 public static boolean updateProject(List<ProjectState> projects) { 108 List<IJavaProject> javaProjectList = new ArrayList<IJavaProject>(projects.size()); 109 for (ProjectState p : projects) { 110 IJavaProject javaProject = JavaCore.create(p.getProject()); 111 if (javaProject != null) { 112 javaProjectList.add(javaProject); 113 } 114 } 115 116 IJavaProject[] javaProjects = javaProjectList.toArray( 117 new IJavaProject[javaProjectList.size()]); 118 119 return updateProjects(javaProjects); 120 } 121 122 @Override 123 public void initialize(IPath containerPath, IJavaProject project) throws CoreException { 124 if (AdtConstants.CONTAINER_PRIVATE_LIBRARIES.equals(containerPath.toString())) { 125 IClasspathContainer libraries = allocateLibraryContainer(project); 126 if (libraries != null) { 127 JavaCore.setClasspathContainer(new Path(AdtConstants.CONTAINER_PRIVATE_LIBRARIES), 128 new IJavaProject[] { project }, 129 new IClasspathContainer[] { libraries }, 130 new NullProgressMonitor()); 131 } 132 133 } else if(AdtConstants.CONTAINER_DEPENDENCIES.equals(containerPath.toString())) { 134 IClasspathContainer dependencies = allocateDependencyContainer(project); 135 if (dependencies != null) { 136 JavaCore.setClasspathContainer(new Path(AdtConstants.CONTAINER_DEPENDENCIES), 137 new IJavaProject[] { project }, 138 new IClasspathContainer[] { dependencies }, 139 new NullProgressMonitor()); 140 } 141 } 142 } 143 144 private static IClasspathContainer allocateLibraryContainer(IJavaProject javaProject) { 145 final IProject iProject = javaProject.getProject(); 146 147 // check if the project has a valid target. 148 ProjectState state = Sdk.getProjectState(iProject); 149 if (state == null) { 150 // getProjectState should already have logged an error. Just bail out. 151 return null; 152 } 153 154 /* 155 * At this point we're going to gather a list of all that need to go in the 156 * dependency container. 157 * - Library project outputs (direct and indirect) 158 * - Java project output (those can be indirectly referenced through library projects 159 * or other other Java projects) 160 * - Jar files: 161 * + inside this project's libs/ 162 * + inside the library projects' libs/ 163 * + inside the referenced Java projects' classpath 164 */ 165 List<IClasspathEntry> entries = new ArrayList<IClasspathEntry>(); 166 167 // list of java project dependencies and jar files that will be built while 168 // going through the library projects. 169 Set<File> jarFiles = new HashSet<File>(); 170 Set<IProject> refProjects = new HashSet<IProject>(); 171 172 // process all the libraries 173 174 List<IProject> libProjects = state.getFullLibraryProjects(); 175 for (IProject libProject : libProjects) { 176 // process all of the library project's dependencies 177 getDependencyListFromClasspath(libProject, refProjects, jarFiles, true); 178 } 179 180 // now process this projects' referenced projects only. 181 processReferencedProjects(iProject, refProjects, jarFiles); 182 183 // and the content of its libs folder 184 getJarListFromLibsFolder(iProject, jarFiles); 185 186 // now add a classpath entry for each Java project (this is a set so dups are already 187 // removed) 188 for (IProject p : refProjects) { 189 entries.add(JavaCore.newProjectEntry(p.getFullPath(), true /*isExported*/)); 190 } 191 192 entries.addAll(convertJarsToClasspathEntries(iProject, jarFiles)); 193 194 return allocateContainer(javaProject, entries, new Path(AdtConstants.CONTAINER_PRIVATE_LIBRARIES), 195 "Android Private Libraries"); 196 } 197 198 private static List<IClasspathEntry> convertJarsToClasspathEntries(final IProject iProject, 199 Set<File> jarFiles) { 200 List<IClasspathEntry> entries = new ArrayList<IClasspathEntry>(jarFiles.size()); 201 202 // and process the jar files list, but first sanitize it to remove dups. 203 JarListSanitizer sanitizer = new JarListSanitizer( 204 iProject.getFolder(SdkConstants.FD_OUTPUT).getLocation().toFile(), 205 new AndroidPrintStream(iProject, null /*prefix*/, 206 AdtPlugin.getOutStream())); 207 208 String errorMessage = null; 209 210 try { 211 List<File> sanitizedList = sanitizer.sanitize(jarFiles); 212 213 for (File jarFile : sanitizedList) { 214 if (jarFile instanceof CPEFile) { 215 CPEFile cpeFile = (CPEFile) jarFile; 216 IClasspathEntry e = cpeFile.getClasspathEntry(); 217 218 entries.add(JavaCore.newLibraryEntry( 219 e.getPath(), 220 e.getSourceAttachmentPath(), 221 e.getSourceAttachmentRootPath(), 222 e.getAccessRules(), 223 e.getExtraAttributes(), 224 true /*isExported*/)); 225 } else { 226 String jarPath = jarFile.getAbsolutePath(); 227 228 IPath sourceAttachmentPath = null; 229 IClasspathAttribute javaDocAttribute = null; 230 231 File jarProperties = new File(jarPath + DOT_PROPERTIES); 232 if (jarProperties.isFile()) { 233 Properties p = new Properties(); 234 InputStream is = null; 235 try { 236 p.load(is = new FileInputStream(jarProperties)); 237 238 String value = p.getProperty(ATTR_SRC); 239 if (value != null) { 240 File srcPath = getFile(jarFile, value); 241 242 if (srcPath.exists()) { 243 sourceAttachmentPath = new Path(srcPath.getAbsolutePath()); 244 } 245 } 246 247 value = p.getProperty(ATTR_DOC); 248 if (value != null) { 249 File docPath = getFile(jarFile, value); 250 if (docPath.exists()) { 251 try { 252 javaDocAttribute = JavaCore.newClasspathAttribute( 253 IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, 254 docPath.toURI().toURL().toString()); 255 } catch (MalformedURLException e) { 256 AdtPlugin.log(e, "Failed to process 'doc' attribute for %s", 257 jarProperties.getAbsolutePath()); 258 } 259 } 260 } 261 262 } catch (FileNotFoundException e) { 263 // shouldn't happen since we check upfront 264 } catch (IOException e) { 265 AdtPlugin.log(e, "Failed to read %s", jarProperties.getAbsolutePath()); 266 } finally { 267 if (is != null) { 268 try { 269 is.close(); 270 } catch (IOException e) { 271 // ignore 272 } 273 } 274 } 275 } 276 277 if (javaDocAttribute != null) { 278 entries.add(JavaCore.newLibraryEntry(new Path(jarPath), 279 sourceAttachmentPath, null /*sourceAttachmentRootPath*/, 280 new IAccessRule[0], 281 new IClasspathAttribute[] { javaDocAttribute }, 282 true /*isExported*/)); 283 } else { 284 entries.add(JavaCore.newLibraryEntry(new Path(jarPath), 285 sourceAttachmentPath, null /*sourceAttachmentRootPath*/, 286 true /*isExported*/)); 287 } 288 } 289 } 290 } catch (DifferentLibException e) { 291 errorMessage = e.getMessage(); 292 AdtPlugin.printErrorToConsole(iProject, (Object[]) e.getDetails()); 293 } catch (Sha1Exception e) { 294 errorMessage = e.getMessage(); 295 } 296 297 processError(iProject, errorMessage, AdtConstants.MARKER_DEPENDENCY, 298 true /*outputToConsole*/); 299 300 return entries; 301 } 302 303 private static IClasspathContainer allocateDependencyContainer(IJavaProject javaProject) { 304 final IProject iProject = javaProject.getProject(); 305 final List<IClasspathEntry> entries = new ArrayList<IClasspathEntry>(); 306 final Set<File> jarFiles = new HashSet<File>(); 307 final IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); 308 309 // check if the project has a valid target. 310 final ProjectState state = Sdk.getProjectState(iProject); 311 if (state == null) { 312 // getProjectState should already have logged an error. Just bail out. 313 return null; 314 } 315 316 // annotations support for older version of android 317 if (state.getTarget() != null && state.getTarget().getVersion().getApiLevel() <= 15) { 318 File annotationsJar = new File(Sdk.getCurrent().getSdkLocation(), 319 SdkConstants.FD_TOOLS + File.separator + SdkConstants.FD_SUPPORT + 320 File.separator + SdkConstants.FN_ANNOTATIONS_JAR); 321 322 jarFiles.add(annotationsJar); 323 } 324 325 // process all the libraries 326 327 List<IProject> libProjects = state.getFullLibraryProjects(); 328 for (IProject libProject : libProjects) { 329 // get the project output 330 IFolder outputFolder = BaseProjectHelper.getAndroidOutputFolder(libProject); 331 332 if (outputFolder != null) { // can happen when closing/deleting a library) 333 IFile jarIFile = outputFolder.getFile(libProject.getName().toLowerCase() + 334 SdkConstants.DOT_JAR); 335 336 // get the source folder for the library project 337 List<IPath> srcs = BaseProjectHelper.getSourceClasspaths(libProject); 338 // find the first non-derived source folder. 339 IPath sourceFolder = null; 340 for (IPath src : srcs) { 341 IFolder srcFolder = workspaceRoot.getFolder(src); 342 if (srcFolder.isDerived() == false) { 343 sourceFolder = src; 344 break; 345 } 346 } 347 348 // we can directly add a CPE for this jar as there's no risk of a duplicate. 349 IClasspathEntry entry = JavaCore.newLibraryEntry( 350 jarIFile.getLocation(), 351 sourceFolder, // source attachment path 352 null, // default source attachment root path. 353 true /*isExported*/); 354 355 entries.add(entry); 356 } 357 } 358 359 entries.addAll(convertJarsToClasspathEntries(iProject, jarFiles)); 360 361 return allocateContainer(javaProject, entries, new Path(CONTAINER_DEPENDENCIES), 362 "Android Dependencies"); 363 } 364 365 private static IClasspathContainer allocateContainer(IJavaProject javaProject, 366 List<IClasspathEntry> entries, IPath id, String description) { 367 368 if (AdtPlugin.getDefault() == null) { // This is totally weird, but I've seen it happen! 369 return null; 370 } 371 372 // First check that the project has a library-type container. 373 try { 374 IClasspathEntry[] rawClasspath = javaProject.getRawClasspath(); 375 final IClasspathEntry[] oldRawClasspath = rawClasspath; 376 377 boolean foundContainer = false; 378 for (IClasspathEntry entry : rawClasspath) { 379 // get the entry and kind 380 final int kind = entry.getEntryKind(); 381 382 if (kind == IClasspathEntry.CPE_CONTAINER) { 383 String path = entry.getPath().toString(); 384 String idString = id.toString(); 385 if (idString.equals(path)) { 386 foundContainer = true; 387 break; 388 } 389 } 390 } 391 392 // if there isn't any, add it. 393 if (foundContainer == false) { 394 // add the android container to the array 395 rawClasspath = ProjectHelper.addEntryToClasspath(rawClasspath, 396 JavaCore.newContainerEntry(id, true /*isExported*/)); 397 } 398 399 // set the new list of entries to the project 400 if (rawClasspath != oldRawClasspath) { 401 javaProject.setRawClasspath(rawClasspath, new NullProgressMonitor()); 402 } 403 } catch (JavaModelException e) { 404 // This really shouldn't happen, but if it does, simply return null (the calling 405 // method will fails as well) 406 return null; 407 } 408 409 return new AndroidClasspathContainer( 410 entries.toArray(new IClasspathEntry[entries.size()]), 411 id, 412 description, 413 IClasspathContainer.K_APPLICATION); 414 } 415 416 private static File getFile(File root, String value) { 417 File file = new File(value); 418 if (file.isAbsolute() == false) { 419 file = new File(root.getParentFile(), value); 420 } 421 422 return file; 423 } 424 425 /** 426 * Finds all the jar files inside a project's libs folder. 427 * @param project 428 * @param jarFiles 429 */ 430 private static void getJarListFromLibsFolder(IProject project, Set<File> jarFiles) { 431 IFolder libsFolder = project.getFolder(SdkConstants.FD_NATIVE_LIBS); 432 if (libsFolder.exists()) { 433 try { 434 IResource[] members = libsFolder.members(); 435 for (IResource member : members) { 436 if (member.getType() == IResource.FILE && 437 SdkConstants.EXT_JAR.equalsIgnoreCase(member.getFileExtension())) { 438 IPath location = member.getLocation(); 439 if (location != null) { 440 jarFiles.add(location.toFile()); 441 } 442 } 443 } 444 } catch (CoreException e) { 445 // can't get the list? ignore this folder. 446 } 447 } 448 } 449 450 /** 451 * Process reference projects from the main projects to add indirect dependencies coming 452 * from Java project. 453 * @param project the main project 454 * @param projects the project list to add to 455 * @param jarFiles the jar list to add to. 456 */ 457 private static void processReferencedProjects(IProject project, 458 Set<IProject> projects, Set<File> jarFiles) { 459 try { 460 IProject[] refs = project.getReferencedProjects(); 461 for (IProject p : refs) { 462 // ignore if it's an Android project, or if it's not a Java 463 // Project 464 if (p.hasNature(JavaCore.NATURE_ID) 465 && p.hasNature(AdtConstants.NATURE_DEFAULT) == false) { 466 467 // process this project's dependencies 468 getDependencyListFromClasspath(p, projects, jarFiles, true /*includeJarFiles*/); 469 } 470 } 471 } catch (CoreException e) { 472 // can't get the referenced projects? ignore 473 } 474 } 475 476 /** 477 * Finds all the dependencies of a given project and add them to a project list and 478 * a jar list. 479 * Only classpath entries that are exported are added, and only Java project (not Android 480 * project) are added. 481 * 482 * @param project the project to query 483 * @param projects the referenced project list to add to 484 * @param jarFiles the jar list to add to 485 * @param includeJarFiles whether to include jar files or just projects. This is useful when 486 * calling on an Android project (value should be <code>false</code>) 487 */ 488 private static void getDependencyListFromClasspath(IProject project, Set<IProject> projects, 489 Set<File> jarFiles, boolean includeJarFiles) { 490 IJavaProject javaProject = JavaCore.create(project); 491 IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot(); 492 493 // we could use IJavaProject.getResolvedClasspath directly, but we actually 494 // want to see the containers themselves. 495 IClasspathEntry[] classpaths = javaProject.readRawClasspath(); 496 if (classpaths != null) { 497 for (IClasspathEntry e : classpaths) { 498 // ignore entries that are not exported 499 if (!e.getPath().toString().equals(CONTAINER_DEPENDENCIES) && e.isExported()) { 500 processCPE(e, javaProject, wsRoot, projects, jarFiles, includeJarFiles); 501 } 502 } 503 } 504 } 505 506 /** 507 * Processes a {@link IClasspathEntry} and add it to one of the list if applicable. 508 * @param entry the entry to process 509 * @param javaProject the {@link IJavaProject} from which this entry came. 510 * @param wsRoot the {@link IWorkspaceRoot} 511 * @param projects the project list to add to 512 * @param jarFiles the jar list to add to 513 * @param includeJarFiles whether to include jar files or just projects. This is useful when 514 * calling on an Android project (value should be <code>false</code>) 515 */ 516 private static void processCPE(IClasspathEntry entry, IJavaProject javaProject, 517 IWorkspaceRoot wsRoot, 518 Set<IProject> projects, Set<File> jarFiles, boolean includeJarFiles) { 519 520 // if this is a classpath variable reference, we resolve it. 521 if (entry.getEntryKind() == IClasspathEntry.CPE_VARIABLE) { 522 entry = JavaCore.getResolvedClasspathEntry(entry); 523 } 524 525 if (entry.getEntryKind() == IClasspathEntry.CPE_PROJECT) { 526 IProject refProject = wsRoot.getProject(entry.getPath().lastSegment()); 527 try { 528 // ignore if it's an Android project, or if it's not a Java Project 529 if (refProject.hasNature(JavaCore.NATURE_ID) && 530 refProject.hasNature(AdtConstants.NATURE_DEFAULT) == false) { 531 // add this project to the list 532 projects.add(refProject); 533 534 // also get the dependency from this project. 535 getDependencyListFromClasspath(refProject, projects, jarFiles, 536 true /*includeJarFiles*/); 537 } 538 } catch (CoreException exception) { 539 // can't query the project nature? ignore 540 } 541 } else if (entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) { 542 if (includeJarFiles) { 543 handleClasspathLibrary(entry, wsRoot, jarFiles); 544 } 545 } else if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER) { 546 // get the container and its content 547 try { 548 IClasspathContainer container = JavaCore.getClasspathContainer( 549 entry.getPath(), javaProject); 550 // ignore the system and default_system types as they represent 551 // libraries that are part of the runtime. 552 if (container != null && 553 container.getKind() == IClasspathContainer.K_APPLICATION) { 554 IClasspathEntry[] entries = container.getClasspathEntries(); 555 for (IClasspathEntry cpe : entries) { 556 processCPE(cpe, javaProject, wsRoot, projects, jarFiles, includeJarFiles); 557 } 558 } 559 } catch (JavaModelException jme) { 560 // can't resolve the container? ignore it. 561 AdtPlugin.log(jme, "Failed to resolve ClasspathContainer: %s", entry.getPath()); 562 } 563 } 564 } 565 566 private static final class CPEFile extends File { 567 private static final long serialVersionUID = 1L; 568 569 private final IClasspathEntry mClasspathEntry; 570 571 public CPEFile(String pathname, IClasspathEntry classpathEntry) { 572 super(pathname); 573 mClasspathEntry = classpathEntry; 574 } 575 576 public CPEFile(File file, IClasspathEntry classpathEntry) { 577 super(file.getAbsolutePath()); 578 mClasspathEntry = classpathEntry; 579 } 580 581 public IClasspathEntry getClasspathEntry() { 582 return mClasspathEntry; 583 } 584 } 585 586 private static void handleClasspathLibrary(IClasspathEntry e, IWorkspaceRoot wsRoot, 587 Set<File> jarFiles) { 588 // get the IPath 589 IPath path = e.getPath(); 590 591 IResource resource = wsRoot.findMember(path); 592 593 if (SdkConstants.EXT_JAR.equalsIgnoreCase(path.getFileExtension())) { 594 // case of a jar file (which could be relative to the workspace or a full path) 595 if (resource != null && resource.exists() && 596 resource.getType() == IResource.FILE) { 597 jarFiles.add(new CPEFile(resource.getLocation().toFile(), e)); 598 } else { 599 // if the jar path doesn't match a workspace resource, 600 // then we get an OSString and check if this links to a valid file. 601 String osFullPath = path.toOSString(); 602 603 File f = new CPEFile(osFullPath, e); 604 if (f.isFile()) { 605 jarFiles.add(f); 606 } 607 } 608 } 609 } 610 } 611