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