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.ide.eclipse.adt.AdtConstants; 20 import com.android.ide.eclipse.adt.AdtPlugin; 21 import com.android.ide.eclipse.adt.AndroidPrintStream; 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.SdkConstants; 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 176 /* 177 * At this point we're going to gather a list of all that need to go in the 178 * dependency container. 179 * - Library project outputs (direct and indirect) 180 * - Java project output (those can be indirectly referenced through library projects 181 * or other other Java projects) 182 * - Jar files: 183 * + inside this project's libs/ 184 * + inside the library projects' libs/ 185 * + inside the referenced Java projects' classpath 186 */ 187 188 List<IClasspathEntry> entries = new ArrayList<IClasspathEntry>(); 189 190 IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); 191 192 // list of java project dependencies and jar files that will be built while 193 // going through the library projects. 194 Set<File> jarFiles = new HashSet<File>(); 195 Set<IProject> refProjects = new HashSet<IProject>(); 196 197 // process all the libraries 198 199 List<IProject> libProjects = state.getFullLibraryProjects(); 200 for (IProject libProject : libProjects) { 201 // get the project output 202 IFolder outputFolder = BaseProjectHelper.getAndroidOutputFolder(libProject); 203 204 if (outputFolder != null) { // can happen when closing/deleting a library) 205 IFile jarIFile = outputFolder.getFile(libProject.getName().toLowerCase() + 206 AdtConstants.DOT_JAR); 207 208 // get the source folder for the library project 209 List<IPath> srcs = BaseProjectHelper.getSourceClasspaths(libProject); 210 // find the first non-derived source folder. 211 IPath sourceFolder = null; 212 for (IPath src : srcs) { 213 IFolder srcFolder = workspaceRoot.getFolder(src); 214 if (srcFolder.isDerived() == false) { 215 sourceFolder = src; 216 break; 217 } 218 } 219 220 // we can directly add a CPE for this jar as there's no risk of a duplicate. 221 IClasspathEntry entry = JavaCore.newLibraryEntry( 222 jarIFile.getLocation(), 223 sourceFolder, // source attachment path 224 null, // default source attachment root path. 225 true /*isExported*/); 226 227 entries.add(entry); 228 229 // process all of the library project's dependencies 230 getDependencyListFromClasspath(libProject, refProjects, jarFiles, true); 231 // and the content of its libs folder. 232 getJarListFromLibsFolder(libProject, jarFiles); 233 } 234 } 235 236 // now process this projects' referenced projects only. 237 processReferencedProjects(iProject, refProjects, jarFiles); 238 // and the content of its libs folder 239 getJarListFromLibsFolder(iProject, jarFiles); 240 241 // annotations support for older version of android 242 if (state.getTarget() != null && state.getTarget().getVersion().getApiLevel() <= 15) { 243 File annotationsJar = new File(Sdk.getCurrent().getSdkLocation(), 244 SdkConstants.FD_TOOLS + File.separator + SdkConstants.FD_SUPPORT + 245 File.separator + SdkConstants.FN_ANNOTATIONS_JAR); 246 247 jarFiles.add(annotationsJar); 248 } 249 250 // now add a classpath entry for each Java project (this is a set so dups are already 251 // removed) 252 for (IProject p : refProjects) { 253 entries.add(JavaCore.newProjectEntry(p.getFullPath(), true /*isExported*/)); 254 } 255 256 // and process the jar files list, but first sanitize it to remove dups. 257 JarListSanitizer sanitizer = new JarListSanitizer( 258 iProject.getFolder(SdkConstants.FD_OUTPUT).getLocation().toFile(), 259 new AndroidPrintStream(iProject, null /*prefix*/, 260 AdtPlugin.getOutStream())); 261 262 String errorMessage = null; 263 264 try { 265 List<File> sanitizedList = sanitizer.sanitize(jarFiles); 266 267 for (File jarFile : sanitizedList) { 268 if (jarFile instanceof CPEFile) { 269 CPEFile cpeFile = (CPEFile) jarFile; 270 IClasspathEntry e = cpeFile.getClasspathEntry(); 271 272 entries.add(JavaCore.newLibraryEntry( 273 e.getPath(), 274 e.getSourceAttachmentPath(), 275 e.getSourceAttachmentRootPath(), 276 e.getAccessRules(), 277 e.getExtraAttributes(), 278 true /*isExported*/)); 279 } else { 280 String jarPath = jarFile.getAbsolutePath(); 281 282 IPath sourceAttachmentPath = null; 283 IClasspathAttribute javaDocAttribute = null; 284 285 File jarProperties = new File(jarPath + DOT_PROPERTIES); 286 if (jarProperties.isFile()) { 287 Properties p = new Properties(); 288 InputStream is = null; 289 try { 290 p.load(is = new FileInputStream(jarProperties)); 291 292 String value = p.getProperty(ATTR_SRC); 293 if (value != null) { 294 File srcPath = getFile(jarFile, value); 295 296 if (srcPath.exists()) { 297 sourceAttachmentPath = new Path(srcPath.getAbsolutePath()); 298 } 299 } 300 301 value = p.getProperty(ATTR_DOC); 302 if (value != null) { 303 File docPath = getFile(jarFile, value); 304 if (docPath.exists()) { 305 try { 306 javaDocAttribute = JavaCore.newClasspathAttribute( 307 IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, 308 docPath.toURI().toURL().toString()); 309 } catch (MalformedURLException e) { 310 AdtPlugin.log(e, "Failed to process 'doc' attribute for %s", 311 jarProperties.getAbsolutePath()); 312 } 313 } 314 } 315 316 } catch (FileNotFoundException e) { 317 // shouldn't happen since we check upfront 318 } catch (IOException e) { 319 AdtPlugin.log(e, "Failed to read %s", jarProperties.getAbsolutePath()); 320 } finally { 321 if (is != null) { 322 try { 323 is.close(); 324 } catch (IOException e) { 325 // ignore 326 } 327 } 328 } 329 } 330 331 if (javaDocAttribute != null) { 332 entries.add(JavaCore.newLibraryEntry(new Path(jarPath), 333 sourceAttachmentPath, null /*sourceAttachmentRootPath*/, 334 new IAccessRule[0], 335 new IClasspathAttribute[] { javaDocAttribute }, 336 true /*isExported*/)); 337 } else { 338 entries.add(JavaCore.newLibraryEntry(new Path(jarPath), 339 sourceAttachmentPath, null /*sourceAttachmentRootPath*/, 340 true /*isExported*/)); 341 } 342 } 343 } 344 } catch (DifferentLibException e) { 345 errorMessage = e.getMessage(); 346 AdtPlugin.printErrorToConsole(iProject, (Object[]) e.getDetails()); 347 } catch (Sha1Exception e) { 348 errorMessage = e.getMessage(); 349 } 350 351 processError(iProject, errorMessage, AdtConstants.MARKER_DEPENDENCY, 352 true /*outputToConsole*/); 353 354 return new AndroidClasspathContainer( 355 entries.toArray(new IClasspathEntry[entries.size()]), 356 new Path(AdtConstants.CONTAINER_LIBRARIES), 357 "Android Dependencies", 358 IClasspathContainer.K_APPLICATION); 359 } 360 361 private static File getFile(File root, String value) { 362 File file = new File(value); 363 if (file.isAbsolute() == false) { 364 file = new File(root.getParentFile(), value); 365 } 366 367 return file; 368 } 369 370 /** 371 * Finds all the jar files inside a project's libs folder. 372 * @param project 373 * @param jarFiles 374 */ 375 private static void getJarListFromLibsFolder(IProject project, Set<File> jarFiles) { 376 IFolder libsFolder = project.getFolder(SdkConstants.FD_NATIVE_LIBS); 377 if (libsFolder.exists()) { 378 try { 379 IResource[] members = libsFolder.members(); 380 for (IResource member : members) { 381 if (member.getType() == IResource.FILE && 382 AdtConstants.EXT_JAR.equalsIgnoreCase(member.getFileExtension())) { 383 jarFiles.add(member.getLocation().toFile()); 384 } 385 } 386 } catch (CoreException e) { 387 // can't get the list? ignore this folder. 388 } 389 } 390 } 391 392 /** 393 * Process reference projects from the main projects to add indirect dependencies coming 394 * from Java project. 395 * @param project the main project 396 * @param projects the project list to add to 397 * @param jarFiles the jar list to add to. 398 */ 399 private static void processReferencedProjects(IProject project, 400 Set<IProject> projects, Set<File> jarFiles) { 401 try { 402 IProject[] refs = project.getReferencedProjects(); 403 for (IProject p : refs) { 404 // ignore if it's an Android project, or if it's not a Java 405 // Project 406 if (p.hasNature(JavaCore.NATURE_ID) 407 && p.hasNature(AdtConstants.NATURE_DEFAULT) == false) { 408 409 // process this project's dependencies 410 getDependencyListFromClasspath(p, projects, jarFiles, true /*includeJarFiles*/); 411 } 412 } 413 } catch (CoreException e) { 414 // can't get the referenced projects? ignore 415 } 416 } 417 418 /** 419 * Finds all the dependencies of a given project and add them to a project list and 420 * a jar list. 421 * Only classpath entries that are exported are added, and only Java project (not Android 422 * project) are added. 423 * 424 * @param project the project to query 425 * @param projects the referenced project list to add to 426 * @param jarFiles the jar list to add to 427 * @param includeJarFiles whether to include jar files or just projects. This is useful when 428 * calling on an Android project (value should be <code>false</code>) 429 */ 430 private static void getDependencyListFromClasspath(IProject project, Set<IProject> projects, 431 Set<File> jarFiles, boolean includeJarFiles) { 432 IJavaProject javaProject = JavaCore.create(project); 433 IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot(); 434 435 // we could use IJavaProject.getResolvedClasspath directly, but we actually 436 // want to see the containers themselves. 437 IClasspathEntry[] classpaths = javaProject.readRawClasspath(); 438 if (classpaths != null) { 439 for (IClasspathEntry e : classpaths) { 440 // ignore entries that are not exported 441 if (e.isExported()) { 442 processCPE(e, javaProject, wsRoot, projects, jarFiles, includeJarFiles); 443 } 444 } 445 } 446 } 447 448 /** 449 * Processes a {@link IClasspathEntry} and add it to one of the list if applicable. 450 * @param entry the entry to process 451 * @param javaProject the {@link IJavaProject} from which this entry came. 452 * @param wsRoot the {@link IWorkspaceRoot} 453 * @param projects the project list to add to 454 * @param jarFiles the jar list to add to 455 * @param includeJarFiles whether to include jar files or just projects. This is useful when 456 * calling on an Android project (value should be <code>false</code>) 457 */ 458 private static void processCPE(IClasspathEntry entry, IJavaProject javaProject, 459 IWorkspaceRoot wsRoot, 460 Set<IProject> projects, Set<File> jarFiles, boolean includeJarFiles) { 461 462 // if this is a classpath variable reference, we resolve it. 463 if (entry.getEntryKind() == IClasspathEntry.CPE_VARIABLE) { 464 entry = JavaCore.getResolvedClasspathEntry(entry); 465 } 466 467 if (entry.getEntryKind() == IClasspathEntry.CPE_PROJECT) { 468 IProject refProject = wsRoot.getProject(entry.getPath().lastSegment()); 469 try { 470 // ignore if it's an Android project, or if it's not a Java Project 471 if (refProject.hasNature(JavaCore.NATURE_ID) && 472 refProject.hasNature(AdtConstants.NATURE_DEFAULT) == false) { 473 // add this project to the list 474 projects.add(refProject); 475 476 // also get the dependency from this project. 477 getDependencyListFromClasspath(refProject, projects, jarFiles, 478 true /*includeJarFiles*/); 479 } 480 } catch (CoreException exception) { 481 // can't query the project nature? ignore 482 } 483 } else if (entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) { 484 if (includeJarFiles) { 485 handleClasspathLibrary(entry, wsRoot, jarFiles); 486 } 487 } else if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER) { 488 // get the container and its content 489 try { 490 IClasspathContainer container = JavaCore.getClasspathContainer( 491 entry.getPath(), javaProject); 492 // ignore the system and default_system types as they represent 493 // libraries that are part of the runtime. 494 if (container != null && 495 container.getKind() == IClasspathContainer.K_APPLICATION) { 496 IClasspathEntry[] entries = container.getClasspathEntries(); 497 for (IClasspathEntry cpe : entries) { 498 processCPE(cpe, javaProject, wsRoot, projects, jarFiles, includeJarFiles); 499 } 500 } 501 } catch (JavaModelException jme) { 502 // can't resolve the container? ignore it. 503 AdtPlugin.log(jme, "Failed to resolve ClasspathContainer: %s", entry.getPath()); 504 } 505 } 506 } 507 508 private static final class CPEFile extends File { 509 private static final long serialVersionUID = 1L; 510 511 private final IClasspathEntry mClasspathEntry; 512 513 public CPEFile(String pathname, IClasspathEntry classpathEntry) { 514 super(pathname); 515 mClasspathEntry = classpathEntry; 516 } 517 518 public CPEFile(File file, IClasspathEntry classpathEntry) { 519 super(file.getAbsolutePath()); 520 mClasspathEntry = classpathEntry; 521 } 522 523 public IClasspathEntry getClasspathEntry() { 524 return mClasspathEntry; 525 } 526 } 527 528 private static void handleClasspathLibrary(IClasspathEntry e, IWorkspaceRoot wsRoot, 529 Set<File> jarFiles) { 530 // get the IPath 531 IPath path = e.getPath(); 532 533 IResource resource = wsRoot.findMember(path); 534 535 if (AdtConstants.EXT_JAR.equalsIgnoreCase(path.getFileExtension())) { 536 // case of a jar file (which could be relative to the workspace or a full path) 537 if (resource != null && resource.exists() && 538 resource.getType() == IResource.FILE) { 539 jarFiles.add(new CPEFile(resource.getLocation().toFile(), e)); 540 } else { 541 // if the jar path doesn't match a workspace resource, 542 // then we get an OSString and check if this links to a valid file. 543 String osFullPath = path.toOSString(); 544 545 File f = new CPEFile(osFullPath, e); 546 if (f.isFile()) { 547 jarFiles.add(f); 548 } 549 } 550 } 551 } 552 } 553