2 * Copyright (C) 2007 The Android Open Source Project
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
8 * http://www.eclipse.org/org/documents/epl-v10.php
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.
17 package com.android.ide.eclipse.adt.internal.project;
19 import com.android.ide.eclipse.adt.AdtPlugin;
20 import com.android.ide.eclipse.adt.AndroidConstants;
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;
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;
48 import java.net.URISyntaxException;
49 import java.util.ArrayList;
50 import java.util.HashSet;
51 import java.util.regex.Pattern;
54 * Classpath container initializer responsible for binding {@link AndroidClasspathContainer} to
55 * {@link IProject}s. This removes the hard-coded path to the android.jar.
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$
62 /** path separator to store multiple paths in a single property. This is guaranteed to not
65 private final static String PATH_SEPARATOR = "\u001C"; //$NON-NLS-1$
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;
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;
78 public AndroidClasspathContainerInitializer() {
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
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());
99 * Creates a new {@link IClasspathEntry} of type {@link IClasspathEntry#CPE_CONTAINER}
100 * linking to the Android Framework.
102 public static IClasspathEntry getContainerEntry() {
103 return JavaCore.newContainerEntry(new Path(CONTAINER_ID));
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.
111 public static boolean checkPath(IPath path) {
112 return CONTAINER_ID.equals(path.toString());
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.
121 public static boolean updateProjects(IJavaProject[] androidProjects) {
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
129 int projectCount = androidProjects.length;
131 IClasspathContainer[] containers = new IClasspathContainer[projectCount];
132 for (int i = 0 ; i < projectCount; i++) {
133 containers[i] = allocateAndroidContainer(androidProjects[i]);
136 // give each project their new container in one call.
137 JavaCore.setClasspathContainer(
138 new Path(CONTAINER_ID),
139 androidProjects, containers, new NullProgressMonitor());
142 } catch (JavaModelException e) {
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.
152 private static IClasspathContainer allocateAndroidContainer(IJavaProject javaProject) {
153 final IProject iProject = javaProject.getProject();
155 String markerMessage = null;
156 boolean outputToConsole = true;
159 AdtPlugin plugin = AdtPlugin.getDefault();
161 synchronized (Sdk.getLock()) {
162 boolean sdkIsLoaded = plugin.getSdkLoadStatus() == LoadStatus.LOADED;
164 // check if the project has a valid target.
165 ProjectState state = Sdk.getProjectState(iProject);
167 // looks like the project state (default.properties) couldn't be read!
168 markerMessage = String.format(
169 "Project has no default.properties file! Edit the project properties to set one.");
171 // this might be null if the sdk is not yet loaded.
172 IAndroidTarget target = state.getTarget();
174 // if we are loaded and the target is non null, we create a valid
175 // ClassPathContainer
176 if (sdkIsLoaded && target != null) {
177 // first make sure the target has loaded its data
178 Sdk.getCurrent().checkAndLoadTargetData(target, null /*project*/);
180 String targetName = target.getClasspathName();
182 return new AndroidClasspathContainer(
183 createClasspathEntries(iProject, target, targetName),
184 new Path(CONTAINER_ID), targetName);
187 // In case of error, we'll try different thing to provide the best error message
189 // Get the project's target's hash string (if it exists)
190 String hashString = state.getTargetHashString();
192 if (hashString == null || hashString.length() == 0) {
193 // if there is no hash string we only show this if the SDK is loaded.
194 // For a project opened at start-up with no target, this would be displayed
195 // twice, once when the project is opened, and once after the SDK has
197 // By testing the sdk is loaded, we only show this once in the console.
199 markerMessage = String.format(
200 "Project has no target set. Edit the project properties to set one.");
202 } else if (sdkIsLoaded) {
203 markerMessage = String.format(
204 "Unable to resolve target '%s'", hashString);
206 // this is the case where there is a hashString but the SDK is not yet
207 // loaded and therefore we can't get the target yet.
208 // We check if there is a cache of the needed information.
209 AndroidClasspathContainer container = getContainerFromCache(iProject);
211 if (container == null) {
212 // either the cache was wrong (ie folder does not exists anymore), or
213 // there was no cache. In this case we need to make sure the project
214 // is resolved again after the SDK is loaded.
215 plugin.setProjectToResolve(javaProject);
217 markerMessage = String.format(
218 "Unable to resolve target '%s' until the SDK is loaded.",
221 // let's not log this one to the console as it will happen at
222 // every boot, and it's expected. (we do keep the error marker though).
223 outputToConsole = false;
226 // we created a container from the cache, so we register the project
227 // to be checked for cache validity once the SDK is loaded
228 plugin.setProjectToCheck(javaProject);
230 // and return the container
236 // return a dummy container to replace the one we may have had before.
237 // It'll be replaced by the real when if/when the target is resolved if/when the
238 // SDK finishes loading.
239 return new IClasspathContainer() {
240 public IClasspathEntry[] getClasspathEntries() {
241 return new IClasspathEntry[0];
244 public String getDescription() {
245 return "Unable to get system library for the project";
248 public int getKind() {
249 return IClasspathContainer.K_DEFAULT_SYSTEM;
252 public IPath getPath() {
258 if (markerMessage != null) {
259 // log the error and put the marker on the project if we can.
260 if (outputToConsole) {
261 AdtPlugin.printErrorToConsole(iProject, markerMessage);
265 BaseProjectHelper.markProject(iProject, AndroidConstants.MARKER_TARGET,
266 markerMessage, IMarker.SEVERITY_ERROR, IMarker.PRIORITY_HIGH);
267 } catch (CoreException e) {
268 // In some cases, the workspace may be locked for modification when we
270 // We schedule a new job to put the marker after.
271 final String fmessage = markerMessage;
272 Job markerJob = new Job("Android SDK: Resolving error markers") {
274 protected IStatus run(IProgressMonitor monitor) {
276 BaseProjectHelper.markProject(iProject,
277 AndroidConstants.MARKER_TARGET,
278 fmessage, IMarker.SEVERITY_ERROR,
279 IMarker.PRIORITY_HIGH);
280 } catch (CoreException e2) {
281 return e2.getStatus();
284 return Status.OK_STATUS;
288 // build jobs are run after other interactive jobs
289 markerJob.setPriority(Job.BUILD);
290 markerJob.schedule();
293 // no error, remove potential MARKER_TARGETs.
295 if (iProject.exists()) {
296 iProject.deleteMarkers(AndroidConstants.MARKER_TARGET, true,
297 IResource.DEPTH_INFINITE);
299 } catch (CoreException ce) {
300 // In some cases, the workspace may be locked for modification when we pass
301 // here, so we schedule a new job to put the marker after.
302 Job markerJob = new Job("Android SDK: Resolving error markers") {
304 protected IStatus run(IProgressMonitor monitor) {
306 iProject.deleteMarkers(AndroidConstants.MARKER_TARGET, true,
307 IResource.DEPTH_INFINITE);
308 } catch (CoreException e2) {
309 return e2.getStatus();
312 return Status.OK_STATUS;
316 // build jobs are run after other interactive jobs
317 markerJob.setPriority(Job.BUILD);
318 markerJob.schedule();
325 * Creates and returns an array of {@link IClasspathEntry} objects for the android
326 * framework and optional libraries.
327 * <p/>This references the OS path to the android.jar and the
328 * java doc directory. This is dynamically created when a project is opened,
329 * and never saved in the project itself, so there's no risk of storing an
331 * The method also stores the paths used to create the entries in the project persistent
332 * properties. A new {@link AndroidClasspathContainer} can be created from the stored path
333 * using the {@link #getContainerFromCache(IProject)} method.
335 * @param target The target that contains the libraries.
338 private static IClasspathEntry[] createClasspathEntries(IProject project,
339 IAndroidTarget target, String targetName) {
341 // get the path from the target
342 String[] paths = getTargetPaths(target);
344 // create the classpath entry from the paths
345 IClasspathEntry[] entries = createClasspathEntriesFromPaths(paths);
347 // paths now contains all the path required to recreate the IClasspathEntry with no
348 // target info. We encode them in a single string, with each path separated by
349 // OS path separator.
350 StringBuilder sb = new StringBuilder(CACHE_VERSION);
351 for (String p : paths) {
352 sb.append(PATH_SEPARATOR);
356 // store this in a project persistent property
357 ProjectHelper.saveStringProperty(project, PROPERTY_CONTAINER_CACHE, sb.toString());
358 ProjectHelper.saveStringProperty(project, PROPERTY_TARGET_NAME, targetName);
364 * Generates an {@link AndroidClasspathContainer} from the project cache, if possible.
366 private static AndroidClasspathContainer getContainerFromCache(IProject project) {
367 // get the cached info from the project persistent properties.
368 String cache = ProjectHelper.loadStringProperty(project, PROPERTY_CONTAINER_CACHE);
369 String targetNameCache = ProjectHelper.loadStringProperty(project, PROPERTY_TARGET_NAME);
370 if (cache == null || targetNameCache == null) {
374 // the first 2 chars must match CACHE_VERSION. The 3rd char is the normal separator.
375 if (cache.startsWith(CACHE_VERSION_SEP) == false) {
379 cache = cache.substring(CACHE_VERSION_SEP.length());
381 // the cache contains multiple paths, separated by a character guaranteed to not be in
382 // the path (\u001C).
383 // The first 3 are for android.jar (jar, source, doc), the rest are for the optional
384 // libraries and should contain at least one doc and a jar (if there are any libraries).
385 // Therefore, the path count should be 3 or 5+
386 String[] paths = cache.split(Pattern.quote(PATH_SEPARATOR));
387 if (paths.length < 3 || paths.length == 4) {
391 // now we check the paths actually exist.
392 // There's an exception: If the source folder for android.jar does not exist, this is
393 // not a problem, so we skip it.
394 // Also paths[CACHE_INDEX_DOCS_URI] is a URI to the javadoc, so we test it a
397 if (new File(paths[CACHE_INDEX_JAR]).exists() == false ||
398 new File(new URI(paths[CACHE_INDEX_DOCS_URI])).exists() == false) {
402 // check the path for the add-ons, if they exist.
403 if (paths.length > CACHE_INDEX_ADD_ON_START) {
405 // check the docs path separately from the rest of the paths as it's a URI.
406 if (new File(new URI(paths[CACHE_INDEX_OPT_DOCS_URI])).exists() == false) {
410 // now just check the remaining paths.
411 for (int i = CACHE_INDEX_ADD_ON_START + 1; i < paths.length; i++) {
412 String path = paths[i];
413 if (path.length() > 0) {
414 File f = new File(path);
415 if (f.exists() == false) {
421 } catch (URISyntaxException e) {
425 IClasspathEntry[] entries = createClasspathEntriesFromPaths(paths);
427 return new AndroidClasspathContainer(entries,
428 new Path(CONTAINER_ID), targetNameCache);
432 * Generates an array of {@link IClasspathEntry} from a set of paths.
433 * @see #getTargetPaths(IAndroidTarget)
435 private static IClasspathEntry[] createClasspathEntriesFromPaths(String[] paths) {
436 ArrayList<IClasspathEntry> list = new ArrayList<IClasspathEntry>();
438 // First, we create the IClasspathEntry for the framework.
439 // now add the android framework to the class path.
440 // create the path object.
441 IPath android_lib = new Path(paths[CACHE_INDEX_JAR]);
442 IPath android_src = new Path(paths[CACHE_INDEX_SRC]);
444 // create the java doc link.
445 IClasspathAttribute cpAttribute = JavaCore.newClasspathAttribute(
446 IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME,
447 paths[CACHE_INDEX_DOCS_URI]);
449 // create the access rule to restrict access to classes in com.android.internal
450 IAccessRule accessRule = JavaCore.newAccessRule(
451 new Path("com/android/internal/**"), //$NON-NLS-1$
452 IAccessRule.K_NON_ACCESSIBLE);
454 IClasspathEntry frameworkClasspathEntry = JavaCore.newLibraryEntry(android_lib,
455 android_src, // source attachment path
456 null, // default source attachment root path.
457 new IAccessRule[] { accessRule },
458 new IClasspathAttribute[] { cpAttribute },
459 false // not exported.
462 list.add(frameworkClasspathEntry);
464 // now deal with optional libraries
465 if (paths.length >= 5) {
466 String docPath = paths[CACHE_INDEX_OPT_DOCS_URI];
468 while (i < paths.length) {
469 Path jarPath = new Path(paths[i++]);
471 IClasspathAttribute[] attributes = null;
472 if (docPath.length() > 0) {
473 attributes = new IClasspathAttribute[] {
474 JavaCore.newClasspathAttribute(
475 IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME,
480 IClasspathEntry entry = JavaCore.newLibraryEntry(
482 null, // source attachment path
483 null, // default source attachment root path.
486 false // not exported.
492 return list.toArray(new IClasspathEntry[list.size()]);
496 * Checks the projects' caches. If the cache was valid, the project is removed from the list.
497 * @param projects the list of projects to check.
499 public static void checkProjectsCache(ArrayList<IJavaProject> projects) {
500 Sdk currentSdk = Sdk.getCurrent();
502 projectLoop: while (i < projects.size()) {
503 IJavaProject javaProject = projects.get(i);
504 IProject iProject = javaProject.getProject();
506 // check if the project is opened
507 if (iProject.isOpen() == false) {
508 // remove from the list
509 // we do not increment i in this case.
515 // project that have been resolved before the sdk was loaded
516 // will have a ProjectState where the IAndroidTarget is null
517 // so we load the target now that the SDK is loaded.
518 IAndroidTarget target = currentSdk.loadTarget(Sdk.getProjectState(iProject));
519 if (target == null) {
520 // this is really not supposed to happen. This would mean there are cached paths,
521 // but default.properties was deleted. Keep the project in the list to force
522 // a resolve which will display the error.
527 String[] targetPaths = getTargetPaths(target);
529 // now get the cached paths
530 String cache = ProjectHelper.loadStringProperty(iProject, PROPERTY_CONTAINER_CACHE);
532 // this should not happen. We'll force resolve again anyway.
537 String[] cachedPaths = cache.split(Pattern.quote(PATH_SEPARATOR));
538 if (cachedPaths.length < 3 || cachedPaths.length == 4) {
539 // paths length is wrong. simply resolve the project again
544 // Now we compare the paths. The first 4 can be compared directly.
545 // because of case sensitiveness we need to use File objects
547 if (targetPaths.length != cachedPaths.length) {
548 // different paths, force resolve again.
553 // compare the main paths (android.jar, main sources, main javadoc)
554 if (new File(targetPaths[CACHE_INDEX_JAR]).equals(
555 new File(cachedPaths[CACHE_INDEX_JAR])) == false ||
556 new File(targetPaths[CACHE_INDEX_SRC]).equals(
557 new File(cachedPaths[CACHE_INDEX_SRC])) == false ||
558 new File(targetPaths[CACHE_INDEX_DOCS_URI]).equals(
559 new File(cachedPaths[CACHE_INDEX_DOCS_URI])) == false) {
560 // different paths, force resolve again.
565 if (cachedPaths.length > CACHE_INDEX_OPT_DOCS_URI) {
566 // compare optional libraries javadoc
567 if (new File(targetPaths[CACHE_INDEX_OPT_DOCS_URI]).equals(
568 new File(cachedPaths[CACHE_INDEX_OPT_DOCS_URI])) == false) {
569 // different paths, force resolve again.
574 // testing the optional jar files is a little bit trickier.
575 // The order is not guaranteed to be identical.
576 // From a previous test, we do know however that there is the same number.
577 // The number of libraries should be low enough that we can simply go through the
579 targetLoop: for (int tpi = 4 ; tpi < targetPaths.length; tpi++) {
580 String targetPath = targetPaths[tpi];
582 // look for a match in the other array
583 for (int cpi = 4 ; cpi < cachedPaths.length; cpi++) {
584 if (new File(targetPath).equals(new File(cachedPaths[cpi]))) {
585 // found a match. Try the next targetPath
590 // if we stop here, we haven't found a match, which means there's a
591 // discrepancy in the libraries. We force a resolve.
593 continue projectLoop;
597 // at the point the check passes, and we can remove the project from the list.
598 // we do not increment i in this case.
604 * Returns the paths necessary to create the {@link IClasspathEntry} for this targets.
605 * <p/>The paths are always in the same order.
607 * <li>Path to android.jar</li>
608 * <li>Path to the source code for android.jar</li>
609 * <li>Path to the javadoc for the android platform</li>
611 * Additionally, if there are optional libraries, the array will contain:
613 * <li>Path to the libraries javadoc</li>
614 * <li>Path to the first .jar file</li>
615 * <li>(more .jar as needed)</li>
618 private static String[] getTargetPaths(IAndroidTarget target) {
619 ArrayList<String> paths = new ArrayList<String>();
621 // first, we get the path for android.jar
622 // The order is: android.jar, source folder, docs folder
623 paths.add(target.getPath(IAndroidTarget.ANDROID_JAR));
624 paths.add(target.getPath(IAndroidTarget.SOURCES));
625 paths.add(AdtPlugin.getUrlDoc());
627 // now deal with optional libraries.
628 IOptionalLibrary[] libraries = target.getOptionalLibraries();
629 if (libraries != null) {
630 // all the optional libraries use the same javadoc, so we start with this
631 String targetDocPath = target.getPath(IAndroidTarget.DOCS);
632 if (targetDocPath != null) {
633 paths.add(ProjectHelper.getJavaDocPath(targetDocPath));
635 // we add an empty string, to always have the same count.
639 // because different libraries could use the same jar file, we make sure we add
640 // each jar file only once.
641 HashSet<String> visitedJars = new HashSet<String>();
642 for (IOptionalLibrary library : libraries) {
643 String jarPath = library.getJarPath();
644 if (visitedJars.contains(jarPath) == false) {
645 visitedJars.add(jarPath);
651 return paths.toArray(new String[paths.size()]);