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