• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.common.sdk.LoadStatus;
20 import com.android.ide.eclipse.adt.AdtConstants;
21 import com.android.ide.eclipse.adt.AdtPlugin;
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.AndroidVersion;
25 import com.android.sdklib.IAndroidTarget;
26 import com.android.sdklib.SdkConstants;
27 import com.android.sdklib.IAndroidTarget.IOptionalLibrary;
28 
29 import org.eclipse.core.resources.IMarker;
30 import org.eclipse.core.resources.IProject;
31 import org.eclipse.core.resources.IResource;
32 import org.eclipse.core.resources.IWorkspaceRoot;
33 import org.eclipse.core.resources.ResourcesPlugin;
34 import org.eclipse.core.runtime.CoreException;
35 import org.eclipse.core.runtime.FileLocator;
36 import org.eclipse.core.runtime.IPath;
37 import org.eclipse.core.runtime.IProgressMonitor;
38 import org.eclipse.core.runtime.IStatus;
39 import org.eclipse.core.runtime.NullProgressMonitor;
40 import org.eclipse.core.runtime.Path;
41 import org.eclipse.core.runtime.Platform;
42 import org.eclipse.core.runtime.Status;
43 import org.eclipse.core.runtime.jobs.Job;
44 import org.eclipse.jdt.core.ClasspathContainerInitializer;
45 import org.eclipse.jdt.core.IAccessRule;
46 import org.eclipse.jdt.core.IClasspathAttribute;
47 import org.eclipse.jdt.core.IClasspathContainer;
48 import org.eclipse.jdt.core.IClasspathEntry;
49 import org.eclipse.jdt.core.IJavaModel;
50 import org.eclipse.jdt.core.IJavaProject;
51 import org.eclipse.jdt.core.JavaCore;
52 import org.eclipse.jdt.core.JavaModelException;
53 import org.osgi.framework.Bundle;
54 
55 import java.io.File;
56 import java.io.IOException;
57 import java.io.InputStream;
58 import java.net.URI;
59 import java.net.URISyntaxException;
60 import java.net.URL;
61 import java.util.ArrayList;
62 import java.util.HashSet;
63 import java.util.regex.Pattern;
64 
65 /**
66  * Classpath container initializer responsible for binding {@link AndroidClasspathContainer} to
67  * {@link IProject}s. This removes the hard-coded path to the android.jar.
68  */
69 public class AndroidClasspathContainerInitializer extends ClasspathContainerInitializer {
70 
71     public static final String NULL_API_URL = "<null>"; //$NON-NLS-1$
72 
73     public static final String SOURCES_ZIP = "/sources.zip"; //$NON-NLS-1$
74 
75     public static final String COM_ANDROID_IDE_ECLIPSE_ADT_SOURCE =
76         "com.android.ide.eclipse.source"; //$NON-NLS-1$
77 
78     private static final String ANDROID_API_REFERENCE =
79         "http://developer.android.com/reference/"; //$NON-NLS-1$
80 
81     private final static String PROPERTY_ANDROID_API = "androidApi"; //$NON-NLS-1$
82 
83     private final static String PROPERTY_ANDROID_SOURCE = "androidSource"; //$NON-NLS-1$
84 
85     /** path separator to store multiple paths in a single property. This is guaranteed to not
86      * be in a path.
87      */
88     private final static String PATH_SEPARATOR = "\u001C"; //$NON-NLS-1$
89 
90     private final static String PROPERTY_CONTAINER_CACHE = "androidContainerCache"; //$NON-NLS-1$
91     private final static String PROPERTY_TARGET_NAME = "androidTargetCache"; //$NON-NLS-1$
92     private final static String CACHE_VERSION = "01"; //$NON-NLS-1$
93     private final static String CACHE_VERSION_SEP = CACHE_VERSION + PATH_SEPARATOR;
94 
95     private final static int CACHE_INDEX_JAR = 0;
96     private final static int CACHE_INDEX_SRC = 1;
97     private final static int CACHE_INDEX_DOCS_URI = 2;
98     private final static int CACHE_INDEX_OPT_DOCS_URI = 3;
99     private final static int CACHE_INDEX_ADD_ON_START = CACHE_INDEX_OPT_DOCS_URI;
100 
AndroidClasspathContainerInitializer()101     public AndroidClasspathContainerInitializer() {
102         // pass
103     }
104 
105     /**
106      * Binds a classpath container  to a {@link IClasspathContainer} for a given project,
107      * or silently fails if unable to do so.
108      * @param containerPath the container path that is the container id.
109      * @param project the project to bind
110      */
111     @Override
initialize(IPath containerPath, IJavaProject project)112     public void initialize(IPath containerPath, IJavaProject project) throws CoreException {
113         if (AdtConstants.CONTAINER_FRAMEWORK.equals(containerPath.toString())) {
114             IClasspathContainer container = allocateAndroidContainer(project);
115             if (container != null) {
116                 JavaCore.setClasspathContainer(new Path(AdtConstants.CONTAINER_FRAMEWORK),
117                         new IJavaProject[] { project },
118                         new IClasspathContainer[] { container },
119                         new NullProgressMonitor());
120             }
121         }
122     }
123 
124     /**
125      * Updates the {@link IJavaProject} objects with new android framework container. This forces
126      * JDT to recompile them.
127      * @param androidProjects the projects to update.
128      * @return <code>true</code> if success, <code>false</code> otherwise.
129      */
updateProjects(IJavaProject[] androidProjects)130     public static boolean updateProjects(IJavaProject[] androidProjects) {
131         try {
132             // Allocate a new AndroidClasspathContainer, and associate it to the android framework
133             // container id for each projects.
134             // By providing a new association between a container id and a IClasspathContainer,
135             // this forces the JDT to query the IClasspathContainer for new IClasspathEntry (with
136             // IClasspathContainer#getClasspathEntries()), and therefore force recompilation of
137             // the projects.
138             int projectCount = androidProjects.length;
139 
140             IClasspathContainer[] containers = new IClasspathContainer[projectCount];
141             for (int i = 0 ; i < projectCount; i++) {
142                 containers[i] = allocateAndroidContainer(androidProjects[i]);
143             }
144 
145             // give each project their new container in one call.
146             JavaCore.setClasspathContainer(
147                     new Path(AdtConstants.CONTAINER_FRAMEWORK),
148                     androidProjects, containers, new NullProgressMonitor());
149 
150             return true;
151         } catch (JavaModelException e) {
152             return false;
153         }
154     }
155 
156     /**
157      * Allocates and returns an {@link AndroidClasspathContainer} object with the proper
158      * path to the framework jar file.
159      * @param javaProject The java project that will receive the container.
160      */
allocateAndroidContainer(IJavaProject javaProject)161     private static IClasspathContainer allocateAndroidContainer(IJavaProject javaProject) {
162         final IProject iProject = javaProject.getProject();
163 
164         String markerMessage = null;
165         boolean outputToConsole = true;
166         IAndroidTarget target = null;
167 
168         try {
169             AdtPlugin plugin = AdtPlugin.getDefault();
170             if (plugin == null) { // This is totally weird, but I've seen it happen!
171                 return null;
172             }
173 
174             synchronized (Sdk.getLock()) {
175                 boolean sdkIsLoaded = plugin.getSdkLoadStatus() == LoadStatus.LOADED;
176 
177                 // check if the project has a valid target.
178                 ProjectState state = Sdk.getProjectState(iProject);
179                 if (state == null) {
180                     // looks like the project state (project.properties) couldn't be read!
181                     markerMessage = String.format(
182                             "Project has no %1$s file! Edit the project properties to set one.",
183                             SdkConstants.FN_PROJECT_PROPERTIES);
184                 } else {
185                     // this might be null if the sdk is not yet loaded.
186                     target = state.getTarget();
187 
188                     // if we are loaded and the target is non null, we create a valid
189                     // ClassPathContainer
190                     if (sdkIsLoaded && target != null) {
191                         // first make sure the target has loaded its data
192                         Sdk.getCurrent().checkAndLoadTargetData(target, null /*project*/);
193 
194                         String targetName = target.getClasspathName();
195 
196                         return new AndroidClasspathContainer(
197                                 createClasspathEntries(iProject, target, targetName),
198                                 new Path(AdtConstants.CONTAINER_FRAMEWORK),
199                                 targetName,
200                                 IClasspathContainer.K_DEFAULT_SYSTEM);
201                     }
202 
203                     // In case of error, we'll try different thing to provide the best error message
204                     // possible.
205                     // Get the project's target's hash string (if it exists)
206                     String hashString = state.getTargetHashString();
207 
208                     if (hashString == null || hashString.length() == 0) {
209                         // if there is no hash string we only show this if the SDK is loaded.
210                         // For a project opened at start-up with no target, this would be displayed
211                         // twice, once when the project is opened, and once after the SDK has
212                         // finished loading.
213                         // By testing the sdk is loaded, we only show this once in the console.
214                         if (sdkIsLoaded) {
215                             markerMessage = String.format(
216                                     "Project has no target set. Edit the project properties to set one.");
217                         }
218                     } else if (sdkIsLoaded) {
219                         markerMessage = String.format(
220                                 "Unable to resolve target '%s'", hashString);
221                     } else {
222                         // this is the case where there is a hashString but the SDK is not yet
223                         // loaded and therefore we can't get the target yet.
224                         // We check if there is a cache of the needed information.
225                         AndroidClasspathContainer container = getContainerFromCache(iProject, target);
226 
227                         if (container == null) {
228                             // either the cache was wrong (ie folder does not exists anymore), or
229                             // there was no cache. In this case we need to make sure the project
230                             // is resolved again after the SDK is loaded.
231                             plugin.setProjectToResolve(javaProject);
232 
233                             markerMessage = String.format(
234                                     "Unable to resolve target '%s' until the SDK is loaded.",
235                                     hashString);
236 
237                             // let's not log this one to the console as it will happen at
238                             // every boot, and it's expected. (we do keep the error marker though).
239                             outputToConsole = false;
240 
241                         } else {
242                             // we created a container from the cache, so we register the project
243                             // to be checked for cache validity once the SDK is loaded
244                             plugin.setProjectToCheck(javaProject);
245 
246                             // and return the container
247                             return container;
248                         }
249                     }
250                 }
251 
252                 // return a dummy container to replace the one we may have had before.
253                 // It'll be replaced by the real when if/when the target is resolved if/when the
254                 // SDK finishes loading.
255                 return new IClasspathContainer() {
256                     public IClasspathEntry[] getClasspathEntries() {
257                         return new IClasspathEntry[0];
258                     }
259 
260                     public String getDescription() {
261                         return "Unable to get system library for the project";
262                     }
263 
264                     public int getKind() {
265                         return IClasspathContainer.K_DEFAULT_SYSTEM;
266                     }
267 
268                     public IPath getPath() {
269                         return null;
270                     }
271                 };
272             }
273         } finally {
274             if (markerMessage != null) {
275                 // log the error and put the marker on the project if we can.
276                 if (outputToConsole) {
277                     AdtPlugin.printErrorToConsole(iProject, markerMessage);
278                 }
279 
280                 try {
281                     BaseProjectHelper.markProject(iProject, AdtConstants.MARKER_TARGET,
282                             markerMessage, IMarker.SEVERITY_ERROR, IMarker.PRIORITY_HIGH);
283                 } catch (CoreException e) {
284                     // In some cases, the workspace may be locked for modification when we
285                     // pass here.
286                     // We schedule a new job to put the marker after.
287                     final String fmessage = markerMessage;
288                     Job markerJob = new Job("Android SDK: Resolving error markers") {
289                         @Override
290                         protected IStatus run(IProgressMonitor monitor) {
291                             try {
292                                 BaseProjectHelper.markProject(iProject,
293                                         AdtConstants.MARKER_TARGET,
294                                         fmessage, IMarker.SEVERITY_ERROR,
295                                         IMarker.PRIORITY_HIGH);
296                             } catch (CoreException e2) {
297                                 return e2.getStatus();
298                             }
299 
300                             return Status.OK_STATUS;
301                         }
302                     };
303 
304                     // build jobs are run after other interactive jobs
305                     markerJob.setPriority(Job.BUILD);
306                     markerJob.schedule();
307                 }
308             } else {
309                 // no error, remove potential MARKER_TARGETs.
310                 try {
311                     if (iProject.exists()) {
312                         iProject.deleteMarkers(AdtConstants.MARKER_TARGET, true,
313                                 IResource.DEPTH_INFINITE);
314                     }
315                 } catch (CoreException ce) {
316                     // In some cases, the workspace may be locked for modification when we pass
317                     // here, so we schedule a new job to put the marker after.
318                     Job markerJob = new Job("Android SDK: Resolving error markers") {
319                         @Override
320                         protected IStatus run(IProgressMonitor monitor) {
321                             try {
322                                 iProject.deleteMarkers(AdtConstants.MARKER_TARGET, true,
323                                         IResource.DEPTH_INFINITE);
324                             } catch (CoreException e2) {
325                                 return e2.getStatus();
326                             }
327 
328                             return Status.OK_STATUS;
329                         }
330                     };
331 
332                     // build jobs are run after other interactive jobs
333                     markerJob.setPriority(Job.BUILD);
334                     markerJob.schedule();
335                 }
336             }
337         }
338     }
339 
340     /**
341      * Creates and returns an array of {@link IClasspathEntry} objects for the android
342      * framework and optional libraries.
343      * <p/>This references the OS path to the android.jar and the
344      * java doc directory. This is dynamically created when a project is opened,
345      * and never saved in the project itself, so there's no risk of storing an
346      * obsolete path.
347      * The method also stores the paths used to create the entries in the project persistent
348      * properties. A new {@link AndroidClasspathContainer} can be created from the stored path
349      * using the {@link #getContainerFromCache(IProject)} method.
350      * @param project
351      * @param target The target that contains the libraries.
352      * @param targetName
353      */
354     private static IClasspathEntry[] createClasspathEntries(IProject project,
355             IAndroidTarget target, String targetName) {
356 
357         // get the path from the target
358         String[] paths = getTargetPaths(target);
359 
360         // create the classpath entry from the paths
361         IClasspathEntry[] entries = createClasspathEntriesFromPaths(paths, target);
362 
363         // paths now contains all the path required to recreate the IClasspathEntry with no
364         // target info. We encode them in a single string, with each path separated by
365         // OS path separator.
366         StringBuilder sb = new StringBuilder(CACHE_VERSION);
367         for (String p : paths) {
368             sb.append(PATH_SEPARATOR);
369             sb.append(p);
370         }
371 
372         // store this in a project persistent property
373         ProjectHelper.saveStringProperty(project, PROPERTY_CONTAINER_CACHE, sb.toString());
374         ProjectHelper.saveStringProperty(project, PROPERTY_TARGET_NAME, targetName);
375 
376         return entries;
377     }
378 
379     /**
380      * Generates an {@link AndroidClasspathContainer} from the project cache, if possible.
381      */
382     private static AndroidClasspathContainer getContainerFromCache(IProject project,
383             IAndroidTarget target) {
384         // get the cached info from the project persistent properties.
385         String cache = ProjectHelper.loadStringProperty(project, PROPERTY_CONTAINER_CACHE);
386         String targetNameCache = ProjectHelper.loadStringProperty(project, PROPERTY_TARGET_NAME);
387         if (cache == null || targetNameCache == null) {
388             return null;
389         }
390 
391         // the first 2 chars must match CACHE_VERSION. The 3rd char is the normal separator.
392         if (cache.startsWith(CACHE_VERSION_SEP) == false) {
393             return null;
394         }
395 
396         cache = cache.substring(CACHE_VERSION_SEP.length());
397 
398         // the cache contains multiple paths, separated by a character guaranteed to not be in
399         // the path (\u001C).
400         // The first 3 are for android.jar (jar, source, doc), the rest are for the optional
401         // libraries and should contain at least one doc and a jar (if there are any libraries).
402         // Therefore, the path count should be 3 or 5+
403         String[] paths = cache.split(Pattern.quote(PATH_SEPARATOR));
404         if (paths.length < 3 || paths.length == 4) {
405             return null;
406         }
407 
408         // now we check the paths actually exist.
409         // There's an exception: If the source folder for android.jar does not exist, this is
410         // not a problem, so we skip it.
411         // Also paths[CACHE_INDEX_DOCS_URI] is a URI to the javadoc, so we test it a
412         // bit differently.
413         try {
414             if (new File(paths[CACHE_INDEX_JAR]).exists() == false ||
415                     new File(new URI(paths[CACHE_INDEX_DOCS_URI])).exists() == false) {
416                 return null;
417             }
418 
419             // check the path for the add-ons, if they exist.
420             if (paths.length > CACHE_INDEX_ADD_ON_START) {
421 
422                 // check the docs path separately from the rest of the paths as it's a URI.
423                 if (new File(new URI(paths[CACHE_INDEX_OPT_DOCS_URI])).exists() == false) {
424                     return null;
425                 }
426 
427                 // now just check the remaining paths.
428                 for (int i = CACHE_INDEX_ADD_ON_START + 1; i < paths.length; i++) {
429                     String path = paths[i];
430                     if (path.length() > 0) {
431                         File f = new File(path);
432                         if (f.exists() == false) {
433                             return null;
434                         }
435                     }
436                 }
437             }
438         } catch (URISyntaxException e) {
439             return null;
440         }
441 
442         IClasspathEntry[] entries = createClasspathEntriesFromPaths(paths, target);
443 
444         return new AndroidClasspathContainer(entries,
445                 new Path(AdtConstants.CONTAINER_FRAMEWORK),
446                 targetNameCache, IClasspathContainer.K_DEFAULT_SYSTEM);
447     }
448 
449     /**
450      * Generates an array of {@link IClasspathEntry} from a set of paths.
451      * @see #getTargetPaths(IAndroidTarget)
452      */
453     private static IClasspathEntry[] createClasspathEntriesFromPaths(String[] paths,
454             IAndroidTarget target) {
455         ArrayList<IClasspathEntry> list = new ArrayList<IClasspathEntry>();
456 
457         // First, we create the IClasspathEntry for the framework.
458         // now add the android framework to the class path.
459         // create the path object.
460         IPath androidLib = new Path(paths[CACHE_INDEX_JAR]);
461 
462         IPath androidSrc = null;
463         String androidSrcOsPath = null;
464         IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
465         if (target != null) {
466             androidSrcOsPath =
467                 ProjectHelper.loadStringProperty(root, getAndroidSourceProperty(target));
468         }
469         if (androidSrcOsPath != null && androidSrcOsPath.trim().length() > 0) {
470             androidSrc = new Path(androidSrcOsPath);
471         }
472         if (androidSrc == null) {
473             androidSrc = new Path(paths[CACHE_INDEX_SRC]);
474             File androidSrcFile = new File(paths[CACHE_INDEX_SRC]);
475             if (!androidSrcFile.isDirectory()) {
476                 androidSrc = null;
477             }
478         }
479 
480         if (androidSrc == null && target != null) {
481             Bundle bundle = getSourceBundle();
482 
483             if (bundle != null) {
484                 AndroidVersion version = target.getVersion();
485                 String apiString = version.getApiString();
486                 String sourcePath = apiString + SOURCES_ZIP;
487                 URL sourceURL = bundle.getEntry(sourcePath);
488                 if (sourceURL != null) {
489                     URL url = null;
490                     try {
491                         url = FileLocator.resolve(sourceURL);
492                     } catch (IOException ignore) {
493                     }
494                     if (url != null) {
495                         androidSrcOsPath = url.getFile();
496                         if (new File(androidSrcOsPath).isFile()) {
497                             androidSrc = new Path(androidSrcOsPath);
498                         }
499                     }
500                 }
501             }
502         }
503 
504         // create the java doc link.
505         String androidApiURL = ProjectHelper.loadStringProperty(root, PROPERTY_ANDROID_API);
506         String apiURL = null;
507         if (androidApiURL != null && testURL(androidApiURL)) {
508             apiURL = androidApiURL;
509         } else {
510             if (testURL(paths[CACHE_INDEX_DOCS_URI])) {
511                 apiURL = paths[CACHE_INDEX_DOCS_URI];
512             } else if (testURL(ANDROID_API_REFERENCE)) {
513                 apiURL = ANDROID_API_REFERENCE;
514             }
515         }
516 
517         IClasspathAttribute[] attributes = null;
518         if (apiURL != null && !NULL_API_URL.equals(apiURL)) {
519             IClasspathAttribute cpAttribute = JavaCore.newClasspathAttribute(
520                     IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, apiURL);
521             attributes = new IClasspathAttribute[] {
522                 cpAttribute
523             };
524         }
525         // create the access rule to restrict access to classes in
526         // com.android.internal
527         IAccessRule accessRule = JavaCore.newAccessRule(new Path("com/android/internal/**"), //$NON-NLS-1$
528                 IAccessRule.K_NON_ACCESSIBLE);
529 
530         IClasspathEntry frameworkClasspathEntry = JavaCore.newLibraryEntry(androidLib,
531                 androidSrc, // source attachment path
532                 null, // default source attachment root path.
533                 new IAccessRule[] { accessRule },
534                 attributes,
535                 false // not exported.
536                 );
537 
538         list.add(frameworkClasspathEntry);
539 
540         // now deal with optional libraries
541         if (paths.length >= 5) {
542             String docPath = paths[CACHE_INDEX_OPT_DOCS_URI];
543             int i = 4;
544             while (i < paths.length) {
545                 Path jarPath = new Path(paths[i++]);
546 
547                 attributes = null;
548                 if (docPath.length() > 0) {
549                     attributes = new IClasspathAttribute[] {
550                         JavaCore.newClasspathAttribute(
551                                 IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, docPath)
552                     };
553                 }
554 
555                 IClasspathEntry entry = JavaCore.newLibraryEntry(
556                         jarPath,
557                         null, // source attachment path
558                         null, // default source attachment root path.
559                         null,
560                         attributes,
561                         false // not exported.
562                         );
563                 list.add(entry);
564             }
565         }
566 
567         if (apiURL != null) {
568             ProjectHelper.saveStringProperty(root, PROPERTY_ANDROID_API, apiURL);
569         }
570         if (androidSrc != null && target != null) {
571             ProjectHelper.saveStringProperty(root, getAndroidSourceProperty(target),
572                     androidSrc.toOSString());
573         }
574         return list.toArray(new IClasspathEntry[list.size()]);
575     }
576 
577     private static Bundle getSourceBundle() {
578         String bundleId = System.getProperty(COM_ANDROID_IDE_ECLIPSE_ADT_SOURCE,
579                 COM_ANDROID_IDE_ECLIPSE_ADT_SOURCE);
580         Bundle bundle = Platform.getBundle(bundleId);
581         return bundle;
582     }
583 
584     private static String getAndroidSourceProperty(IAndroidTarget target) {
585         if (target == null) {
586             return null;
587         }
588         String androidSourceProperty = PROPERTY_ANDROID_SOURCE + "_"
589                 + target.getVersion().getApiString();
590         return androidSourceProperty;
591     }
592 
593     private static boolean testURL(String androidApiURL) {
594         boolean valid = false;
595         InputStream is = null;
596         try {
597             URL testURL = new URL(androidApiURL);
598             is = testURL.openStream();
599             valid = true;
600         } catch (Exception ignore) {
601         } finally {
602             if (is != null) {
603                 try {
604                     is.close();
605                 } catch (IOException ignore) {
606                 }
607             }
608         }
609         return valid;
610     }
611 
612     /**
613      * Checks the projects' caches. If the cache was valid, the project is removed from the list.
614      * @param projects the list of projects to check.
615      */
616     public static void checkProjectsCache(ArrayList<IJavaProject> projects) {
617         Sdk currentSdk = Sdk.getCurrent();
618         int i = 0;
619         projectLoop: while (i < projects.size()) {
620             IJavaProject javaProject = projects.get(i);
621             IProject iProject = javaProject.getProject();
622 
623             // check if the project is opened
624             if (iProject.isOpen() == false) {
625                 // remove from the list
626                 // we do not increment i in this case.
627                 projects.remove(i);
628 
629                 continue;
630             }
631 
632             // project that have been resolved before the sdk was loaded
633             // will have a ProjectState where the IAndroidTarget is null
634             // so we load the target now that the SDK is loaded.
635             IAndroidTarget target = currentSdk.loadTarget(Sdk.getProjectState(iProject));
636             if (target == null) {
637                 // this is really not supposed to happen. This would mean there are cached paths,
638                 // but project.properties was deleted. Keep the project in the list to force
639                 // a resolve which will display the error.
640                 i++;
641                 continue;
642             }
643 
644             String[] targetPaths = getTargetPaths(target);
645 
646             // now get the cached paths
647             String cache = ProjectHelper.loadStringProperty(iProject, PROPERTY_CONTAINER_CACHE);
648             if (cache == null) {
649                 // this should not happen. We'll force resolve again anyway.
650                 i++;
651                 continue;
652             }
653 
654             String[] cachedPaths = cache.split(Pattern.quote(PATH_SEPARATOR));
655             if (cachedPaths.length < 3 || cachedPaths.length == 4) {
656                 // paths length is wrong. simply resolve the project again
657                 i++;
658                 continue;
659             }
660 
661             // Now we compare the paths. The first 4 can be compared directly.
662             // because of case sensitiveness we need to use File objects
663 
664             if (targetPaths.length != cachedPaths.length) {
665                 // different paths, force resolve again.
666                 i++;
667                 continue;
668             }
669 
670             // compare the main paths (android.jar, main sources, main javadoc)
671             if (new File(targetPaths[CACHE_INDEX_JAR]).equals(
672                             new File(cachedPaths[CACHE_INDEX_JAR])) == false ||
673                     new File(targetPaths[CACHE_INDEX_SRC]).equals(
674                             new File(cachedPaths[CACHE_INDEX_SRC])) == false ||
675                     new File(targetPaths[CACHE_INDEX_DOCS_URI]).equals(
676                             new File(cachedPaths[CACHE_INDEX_DOCS_URI])) == false) {
677                 // different paths, force resolve again.
678                 i++;
679                 continue;
680             }
681 
682             if (cachedPaths.length > CACHE_INDEX_OPT_DOCS_URI) {
683                 // compare optional libraries javadoc
684                 if (new File(targetPaths[CACHE_INDEX_OPT_DOCS_URI]).equals(
685                         new File(cachedPaths[CACHE_INDEX_OPT_DOCS_URI])) == false) {
686                     // different paths, force resolve again.
687                     i++;
688                     continue;
689                 }
690 
691                 // testing the optional jar files is a little bit trickier.
692                 // The order is not guaranteed to be identical.
693                 // From a previous test, we do know however that there is the same number.
694                 // The number of libraries should be low enough that we can simply go through the
695                 // lists manually.
696                 targetLoop: for (int tpi = 4 ; tpi < targetPaths.length; tpi++) {
697                     String targetPath = targetPaths[tpi];
698 
699                     // look for a match in the other array
700                     for (int cpi = 4 ; cpi < cachedPaths.length; cpi++) {
701                         if (new File(targetPath).equals(new File(cachedPaths[cpi]))) {
702                             // found a match. Try the next targetPath
703                             continue targetLoop;
704                         }
705                     }
706 
707                     // if we stop here, we haven't found a match, which means there's a
708                     // discrepancy in the libraries. We force a resolve.
709                     i++;
710                     continue projectLoop;
711                 }
712             }
713 
714             // at the point the check passes, and we can remove the project from the list.
715             // we do not increment i in this case.
716             projects.remove(i);
717         }
718     }
719 
720     /**
721      * Returns the paths necessary to create the {@link IClasspathEntry} for this targets.
722      * <p/>The paths are always in the same order.
723      * <ul>
724      * <li>Path to android.jar</li>
725      * <li>Path to the source code for android.jar</li>
726      * <li>Path to the javadoc for the android platform</li>
727      * </ul>
728      * Additionally, if there are optional libraries, the array will contain:
729      * <ul>
730      * <li>Path to the libraries javadoc</li>
731      * <li>Path to the first .jar file</li>
732      * <li>(more .jar as needed)</li>
733      * </ul>
734      */
735     private static String[] getTargetPaths(IAndroidTarget target) {
736         ArrayList<String> paths = new ArrayList<String>();
737 
738         // first, we get the path for android.jar
739         // The order is: android.jar, source folder, docs folder
740         paths.add(target.getPath(IAndroidTarget.ANDROID_JAR));
741         paths.add(target.getPath(IAndroidTarget.SOURCES));
742         paths.add(AdtPlugin.getUrlDoc());
743 
744         // now deal with optional libraries.
745         IOptionalLibrary[] libraries = target.getOptionalLibraries();
746         if (libraries != null) {
747             // all the optional libraries use the same javadoc, so we start with this
748             String targetDocPath = target.getPath(IAndroidTarget.DOCS);
749             if (targetDocPath != null) {
750                 paths.add(ProjectHelper.getJavaDocPath(targetDocPath));
751             } else {
752                 // we add an empty string, to always have the same count.
753                 paths.add("");
754             }
755 
756             // because different libraries could use the same jar file, we make sure we add
757             // each jar file only once.
758             HashSet<String> visitedJars = new HashSet<String>();
759             for (IOptionalLibrary library : libraries) {
760                 String jarPath = library.getJarPath();
761                 if (visitedJars.contains(jarPath) == false) {
762                     visitedJars.add(jarPath);
763                     paths.add(jarPath);
764                 }
765             }
766         }
767 
768         return paths.toArray(new String[paths.size()]);
769     }
770 
771     @Override
772     public boolean canUpdateClasspathContainer(IPath containerPath, IJavaProject project) {
773         return true;
774     }
775 
776     @Override
777     public void requestClasspathContainerUpdate(IPath containerPath, IJavaProject project,
778             IClasspathContainer containerSuggestion) throws CoreException {
779         AdtPlugin plugin = AdtPlugin.getDefault();
780 
781         synchronized (Sdk.getLock()) {
782             boolean sdkIsLoaded = plugin.getSdkLoadStatus() == LoadStatus.LOADED;
783 
784             // check if the project has a valid target.
785             IAndroidTarget target = null;
786             if (sdkIsLoaded) {
787                 target = Sdk.getCurrent().getTarget(project.getProject());
788             }
789             if (sdkIsLoaded && target != null) {
790                 String[] paths = getTargetPaths(target);
791                 IPath android_lib = new Path(paths[CACHE_INDEX_JAR]);
792                 IClasspathEntry[] entries = containerSuggestion.getClasspathEntries();
793                 for (int i = 0; i < entries.length; i++) {
794                     IClasspathEntry entry = entries[i];
795                     if (entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) {
796                         IPath entryPath = entry.getPath();
797 
798                         if (entryPath != null) {
799                             if (entryPath.equals(android_lib)) {
800                                 IPath entrySrcPath = entry.getSourceAttachmentPath();
801                                 IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
802                                 if (entrySrcPath != null) {
803                                     ProjectHelper.saveStringProperty(root,
804                                             getAndroidSourceProperty(target),
805                                             entrySrcPath.toString());
806                                 } else {
807                                     ProjectHelper.saveStringProperty(root,
808                                             getAndroidSourceProperty(target), null);
809                                 }
810                                 IClasspathAttribute[] extraAttributtes = entry.getExtraAttributes();
811                                 if (extraAttributtes.length == 0) {
812                                     ProjectHelper.saveStringProperty(root, PROPERTY_ANDROID_API,
813                                             NULL_API_URL);
814                                 }
815                                 for (int j = 0; j < extraAttributtes.length; j++) {
816                                     IClasspathAttribute extraAttribute = extraAttributtes[j];
817                                     String value = extraAttribute.getValue();
818                                     if ((value == null || value.trim().length() == 0)
819                                             && IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME
820                                                     .equals(extraAttribute.getName())) {
821                                         value = NULL_API_URL;
822                                     }
823                                     if (IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME
824                                             .equals(extraAttribute.getName())) {
825                                         ProjectHelper.saveStringProperty(root,
826                                                 PROPERTY_ANDROID_API, value);
827 
828                                     }
829                                 }
830                             }
831                         }
832                     }
833                 }
834                 rebindClasspathEntries(project.getJavaModel(), containerPath);
835             }
836         }
837     }
838 
839     private static void rebindClasspathEntries(IJavaModel model, IPath containerPath)
840             throws JavaModelException {
841         ArrayList<IJavaProject> affectedProjects = new ArrayList<IJavaProject>();
842 
843         IJavaProject[] projects = model.getJavaProjects();
844         for (int i = 0; i < projects.length; i++) {
845             IJavaProject project = projects[i];
846             IClasspathEntry[] entries = project.getRawClasspath();
847             for (int k = 0; k < entries.length; k++) {
848                 IClasspathEntry curr = entries[k];
849                 if (curr.getEntryKind() == IClasspathEntry.CPE_CONTAINER
850                         && containerPath.equals(curr.getPath())) {
851                     affectedProjects.add(project);
852                 }
853             }
854         }
855         if (!affectedProjects.isEmpty()) {
856             IJavaProject[] affected = affectedProjects
857                     .toArray(new IJavaProject[affectedProjects.size()]);
858             updateProjects(affected);
859         }
860     }
861 
862 }
863