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