/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Eclipse Public License, Version 1.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.eclipse.org/org/documents/epl-v10.php
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.ide.eclipse.adt.internal.project;

import com.android.ide.common.sdk.LoadStatus;
import com.android.ide.eclipse.adt.AdtConstants;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.SdkConstants;
import com.android.sdklib.IAndroidTarget.IOptionalLibrary;

import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jdt.core.ClasspathContainerInitializer;
import org.eclipse.jdt.core.IAccessRule;
import org.eclipse.jdt.core.IClasspathAttribute;
import org.eclipse.jdt.core.IClasspathContainer;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaModel;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.osgi.framework.Bundle;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.regex.Pattern;

/**
 * Classpath container initializer responsible for binding {@link AndroidClasspathContainer} to
 * {@link IProject}s. This removes the hard-coded path to the android.jar.
 */
public class AndroidClasspathContainerInitializer extends ClasspathContainerInitializer {

    public static final String NULL_API_URL = "<null>"; //$NON-NLS-1$

    public static final String SOURCES_ZIP = "/sources.zip"; //$NON-NLS-1$

    public static final String COM_ANDROID_IDE_ECLIPSE_ADT_SOURCE =
        "com.android.ide.eclipse.source"; //$NON-NLS-1$

    private static final String ANDROID_API_REFERENCE =
        "http://developer.android.com/reference/"; //$NON-NLS-1$

    private final static String PROPERTY_ANDROID_API = "androidApi"; //$NON-NLS-1$

    private final static String PROPERTY_ANDROID_SOURCE = "androidSource"; //$NON-NLS-1$

    /** path separator to store multiple paths in a single property. This is guaranteed to not
     * be in a path.
     */
    private final static String PATH_SEPARATOR = "\u001C"; //$NON-NLS-1$

    private final static String PROPERTY_CONTAINER_CACHE = "androidContainerCache"; //$NON-NLS-1$
    private final static String PROPERTY_TARGET_NAME = "androidTargetCache"; //$NON-NLS-1$
    private final static String CACHE_VERSION = "01"; //$NON-NLS-1$
    private final static String CACHE_VERSION_SEP = CACHE_VERSION + PATH_SEPARATOR;

    private final static int CACHE_INDEX_JAR = 0;
    private final static int CACHE_INDEX_SRC = 1;
    private final static int CACHE_INDEX_DOCS_URI = 2;
    private final static int CACHE_INDEX_OPT_DOCS_URI = 3;
    private final static int CACHE_INDEX_ADD_ON_START = CACHE_INDEX_OPT_DOCS_URI;

    public AndroidClasspathContainerInitializer() {
        // pass
    }

    /**
     * Binds a classpath container  to a {@link IClasspathContainer} for a given project,
     * or silently fails if unable to do so.
     * @param containerPath the container path that is the container id.
     * @param project the project to bind
     */
    @Override
    public void initialize(IPath containerPath, IJavaProject project) throws CoreException {
        if (AdtConstants.CONTAINER_FRAMEWORK.equals(containerPath.toString())) {
            IClasspathContainer container = allocateAndroidContainer(project);
            if (container != null) {
                JavaCore.setClasspathContainer(new Path(AdtConstants.CONTAINER_FRAMEWORK),
                        new IJavaProject[] { project },
                        new IClasspathContainer[] { container },
                        new NullProgressMonitor());
            }
        }
    }

    /**
     * Updates the {@link IJavaProject} objects with new android framework container. This forces
     * JDT to recompile them.
     * @param androidProjects the projects to update.
     * @return <code>true</code> if success, <code>false</code> otherwise.
     */
    public static boolean updateProjects(IJavaProject[] androidProjects) {
        try {
            // Allocate a new AndroidClasspathContainer, and associate it to the android framework
            // container id for each projects.
            // By providing a new association between a container id and a IClasspathContainer,
            // this forces the JDT to query the IClasspathContainer for new IClasspathEntry (with
            // IClasspathContainer#getClasspathEntries()), and therefore force recompilation of
            // the projects.
            int projectCount = androidProjects.length;

            IClasspathContainer[] containers = new IClasspathContainer[projectCount];
            for (int i = 0 ; i < projectCount; i++) {
                containers[i] = allocateAndroidContainer(androidProjects[i]);
            }

            // give each project their new container in one call.
            JavaCore.setClasspathContainer(
                    new Path(AdtConstants.CONTAINER_FRAMEWORK),
                    androidProjects, containers, new NullProgressMonitor());

            return true;
        } catch (JavaModelException e) {
            return false;
        }
    }

    /**
     * Allocates and returns an {@link AndroidClasspathContainer} object with the proper
     * path to the framework jar file.
     * @param javaProject The java project that will receive the container.
     */
    private static IClasspathContainer allocateAndroidContainer(IJavaProject javaProject) {
        final IProject iProject = javaProject.getProject();

        String markerMessage = null;
        boolean outputToConsole = true;
        IAndroidTarget target = null;

        try {
            AdtPlugin plugin = AdtPlugin.getDefault();
            if (plugin == null) { // This is totally weird, but I've seen it happen!
                return null;
            }

            synchronized (Sdk.getLock()) {
                boolean sdkIsLoaded = plugin.getSdkLoadStatus() == LoadStatus.LOADED;

                // check if the project has a valid target.
                ProjectState state = Sdk.getProjectState(iProject);
                if (state == null) {
                    // looks like the project state (project.properties) couldn't be read!
                    markerMessage = String.format(
                            "Project has no %1$s file! Edit the project properties to set one.",
                            SdkConstants.FN_PROJECT_PROPERTIES);
                } else {
                    // this might be null if the sdk is not yet loaded.
                    target = state.getTarget();

                    // if we are loaded and the target is non null, we create a valid
                    // ClassPathContainer
                    if (sdkIsLoaded && target != null) {
                        // first make sure the target has loaded its data
                        Sdk.getCurrent().checkAndLoadTargetData(target, null /*project*/);

                        String targetName = target.getClasspathName();

                        return new AndroidClasspathContainer(
                                createClasspathEntries(iProject, target, targetName),
                                new Path(AdtConstants.CONTAINER_FRAMEWORK),
                                targetName,
                                IClasspathContainer.K_DEFAULT_SYSTEM);
                    }

                    // In case of error, we'll try different thing to provide the best error message
                    // possible.
                    // Get the project's target's hash string (if it exists)
                    String hashString = state.getTargetHashString();

                    if (hashString == null || hashString.length() == 0) {
                        // if there is no hash string we only show this if the SDK is loaded.
                        // For a project opened at start-up with no target, this would be displayed
                        // twice, once when the project is opened, and once after the SDK has
                        // finished loading.
                        // By testing the sdk is loaded, we only show this once in the console.
                        if (sdkIsLoaded) {
                            markerMessage = String.format(
                                    "Project has no target set. Edit the project properties to set one.");
                        }
                    } else if (sdkIsLoaded) {
                        markerMessage = String.format(
                                "Unable to resolve target '%s'", hashString);
                    } else {
                        // this is the case where there is a hashString but the SDK is not yet
                        // loaded and therefore we can't get the target yet.
                        // We check if there is a cache of the needed information.
                        AndroidClasspathContainer container = getContainerFromCache(iProject, target);

                        if (container == null) {
                            // either the cache was wrong (ie folder does not exists anymore), or
                            // there was no cache. In this case we need to make sure the project
                            // is resolved again after the SDK is loaded.
                            plugin.setProjectToResolve(javaProject);

                            markerMessage = String.format(
                                    "Unable to resolve target '%s' until the SDK is loaded.",
                                    hashString);

                            // let's not log this one to the console as it will happen at
                            // every boot, and it's expected. (we do keep the error marker though).
                            outputToConsole = false;

                        } else {
                            // we created a container from the cache, so we register the project
                            // to be checked for cache validity once the SDK is loaded
                            plugin.setProjectToCheck(javaProject);

                            // and return the container
                            return container;
                        }
                    }
                }

                // return a dummy container to replace the one we may have had before.
                // It'll be replaced by the real when if/when the target is resolved if/when the
                // SDK finishes loading.
                return new IClasspathContainer() {
                    public IClasspathEntry[] getClasspathEntries() {
                        return new IClasspathEntry[0];
                    }

                    public String getDescription() {
                        return "Unable to get system library for the project";
                    }

                    public int getKind() {
                        return IClasspathContainer.K_DEFAULT_SYSTEM;
                    }

                    public IPath getPath() {
                        return null;
                    }
                };
            }
        } finally {
            if (markerMessage != null) {
                // log the error and put the marker on the project if we can.
                if (outputToConsole) {
                    AdtPlugin.printErrorToConsole(iProject, markerMessage);
                }

                try {
                    BaseProjectHelper.markProject(iProject, AdtConstants.MARKER_TARGET,
                            markerMessage, IMarker.SEVERITY_ERROR, IMarker.PRIORITY_HIGH);
                } catch (CoreException e) {
                    // In some cases, the workspace may be locked for modification when we
                    // pass here.
                    // We schedule a new job to put the marker after.
                    final String fmessage = markerMessage;
                    Job markerJob = new Job("Android SDK: Resolving error markers") {
                        @Override
                        protected IStatus run(IProgressMonitor monitor) {
                            try {
                                BaseProjectHelper.markProject(iProject,
                                        AdtConstants.MARKER_TARGET,
                                        fmessage, IMarker.SEVERITY_ERROR,
                                        IMarker.PRIORITY_HIGH);
                            } catch (CoreException e2) {
                                return e2.getStatus();
                            }

                            return Status.OK_STATUS;
                        }
                    };

                    // build jobs are run after other interactive jobs
                    markerJob.setPriority(Job.BUILD);
                    markerJob.schedule();
                }
            } else {
                // no error, remove potential MARKER_TARGETs.
                try {
                    if (iProject.exists()) {
                        iProject.deleteMarkers(AdtConstants.MARKER_TARGET, true,
                                IResource.DEPTH_INFINITE);
                    }
                } catch (CoreException ce) {
                    // In some cases, the workspace may be locked for modification when we pass
                    // here, so we schedule a new job to put the marker after.
                    Job markerJob = new Job("Android SDK: Resolving error markers") {
                        @Override
                        protected IStatus run(IProgressMonitor monitor) {
                            try {
                                iProject.deleteMarkers(AdtConstants.MARKER_TARGET, true,
                                        IResource.DEPTH_INFINITE);
                            } catch (CoreException e2) {
                                return e2.getStatus();
                            }

                            return Status.OK_STATUS;
                        }
                    };

                    // build jobs are run after other interactive jobs
                    markerJob.setPriority(Job.BUILD);
                    markerJob.schedule();
                }
            }
        }
    }

    /**
     * Creates and returns an array of {@link IClasspathEntry} objects for the android
     * framework and optional libraries.
     * <p/>This references the OS path to the android.jar and the
     * java doc directory. This is dynamically created when a project is opened,
     * and never saved in the project itself, so there's no risk of storing an
     * obsolete path.
     * The method also stores the paths used to create the entries in the project persistent
     * properties. A new {@link AndroidClasspathContainer} can be created from the stored path
     * using the {@link #getContainerFromCache(IProject)} method.
     * @param project
     * @param target The target that contains the libraries.
     * @param targetName
     */
    private static IClasspathEntry[] createClasspathEntries(IProject project,
            IAndroidTarget target, String targetName) {

        // get the path from the target
        String[] paths = getTargetPaths(target);

        // create the classpath entry from the paths
        IClasspathEntry[] entries = createClasspathEntriesFromPaths(paths, target);

        // paths now contains all the path required to recreate the IClasspathEntry with no
        // target info. We encode them in a single string, with each path separated by
        // OS path separator.
        StringBuilder sb = new StringBuilder(CACHE_VERSION);
        for (String p : paths) {
            sb.append(PATH_SEPARATOR);
            sb.append(p);
        }

        // store this in a project persistent property
        ProjectHelper.saveStringProperty(project, PROPERTY_CONTAINER_CACHE, sb.toString());
        ProjectHelper.saveStringProperty(project, PROPERTY_TARGET_NAME, targetName);

        return entries;
    }

    /**
     * Generates an {@link AndroidClasspathContainer} from the project cache, if possible.
     */
    private static AndroidClasspathContainer getContainerFromCache(IProject project,
            IAndroidTarget target) {
        // get the cached info from the project persistent properties.
        String cache = ProjectHelper.loadStringProperty(project, PROPERTY_CONTAINER_CACHE);
        String targetNameCache = ProjectHelper.loadStringProperty(project, PROPERTY_TARGET_NAME);
        if (cache == null || targetNameCache == null) {
            return null;
        }

        // the first 2 chars must match CACHE_VERSION. The 3rd char is the normal separator.
        if (cache.startsWith(CACHE_VERSION_SEP) == false) {
            return null;
        }

        cache = cache.substring(CACHE_VERSION_SEP.length());

        // the cache contains multiple paths, separated by a character guaranteed to not be in
        // the path (\u001C).
        // The first 3 are for android.jar (jar, source, doc), the rest are for the optional
        // libraries and should contain at least one doc and a jar (if there are any libraries).
        // Therefore, the path count should be 3 or 5+
        String[] paths = cache.split(Pattern.quote(PATH_SEPARATOR));
        if (paths.length < 3 || paths.length == 4) {
            return null;
        }

        // now we check the paths actually exist.
        // There's an exception: If the source folder for android.jar does not exist, this is
        // not a problem, so we skip it.
        // Also paths[CACHE_INDEX_DOCS_URI] is a URI to the javadoc, so we test it a
        // bit differently.
        try {
            if (new File(paths[CACHE_INDEX_JAR]).exists() == false ||
                    new File(new URI(paths[CACHE_INDEX_DOCS_URI])).exists() == false) {
                return null;
            }

            // check the path for the add-ons, if they exist.
            if (paths.length > CACHE_INDEX_ADD_ON_START) {

                // check the docs path separately from the rest of the paths as it's a URI.
                if (new File(new URI(paths[CACHE_INDEX_OPT_DOCS_URI])).exists() == false) {
                    return null;
                }

                // now just check the remaining paths.
                for (int i = CACHE_INDEX_ADD_ON_START + 1; i < paths.length; i++) {
                    String path = paths[i];
                    if (path.length() > 0) {
                        File f = new File(path);
                        if (f.exists() == false) {
                            return null;
                        }
                    }
                }
            }
        } catch (URISyntaxException e) {
            return null;
        }

        IClasspathEntry[] entries = createClasspathEntriesFromPaths(paths, target);

        return new AndroidClasspathContainer(entries,
                new Path(AdtConstants.CONTAINER_FRAMEWORK),
                targetNameCache, IClasspathContainer.K_DEFAULT_SYSTEM);
    }

    /**
     * Generates an array of {@link IClasspathEntry} from a set of paths.
     * @see #getTargetPaths(IAndroidTarget)
     */
    private static IClasspathEntry[] createClasspathEntriesFromPaths(String[] paths,
            IAndroidTarget target) {
        ArrayList<IClasspathEntry> list = new ArrayList<IClasspathEntry>();

        // First, we create the IClasspathEntry for the framework.
        // now add the android framework to the class path.
        // create the path object.
        IPath androidLib = new Path(paths[CACHE_INDEX_JAR]);

        IPath androidSrc = null;
        String androidSrcOsPath = null;
        IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
        if (target != null) {
            androidSrcOsPath =
                ProjectHelper.loadStringProperty(root, getAndroidSourceProperty(target));
        }
        if (androidSrcOsPath != null && androidSrcOsPath.trim().length() > 0) {
            androidSrc = new Path(androidSrcOsPath);
        }
        if (androidSrc == null) {
            androidSrc = new Path(paths[CACHE_INDEX_SRC]);
            File androidSrcFile = new File(paths[CACHE_INDEX_SRC]);
            if (!androidSrcFile.isDirectory()) {
                androidSrc = null;
            }
        }

        if (androidSrc == null && target != null) {
            Bundle bundle = getSourceBundle();

            if (bundle != null) {
                AndroidVersion version = target.getVersion();
                String apiString = version.getApiString();
                String sourcePath = apiString + SOURCES_ZIP;
                URL sourceURL = bundle.getEntry(sourcePath);
                if (sourceURL != null) {
                    URL url = null;
                    try {
                        url = FileLocator.resolve(sourceURL);
                    } catch (IOException ignore) {
                    }
                    if (url != null) {
                        androidSrcOsPath = url.getFile();
                        if (new File(androidSrcOsPath).isFile()) {
                            androidSrc = new Path(androidSrcOsPath);
                        }
                    }
                }
            }
        }

        // create the java doc link.
        String androidApiURL = ProjectHelper.loadStringProperty(root, PROPERTY_ANDROID_API);
        String apiURL = null;
        if (androidApiURL != null && testURL(androidApiURL)) {
            apiURL = androidApiURL;
        } else {
            if (testURL(paths[CACHE_INDEX_DOCS_URI])) {
                apiURL = paths[CACHE_INDEX_DOCS_URI];
            } else if (testURL(ANDROID_API_REFERENCE)) {
                apiURL = ANDROID_API_REFERENCE;
            }
        }

        IClasspathAttribute[] attributes = null;
        if (apiURL != null && !NULL_API_URL.equals(apiURL)) {
            IClasspathAttribute cpAttribute = JavaCore.newClasspathAttribute(
                    IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, apiURL);
            attributes = new IClasspathAttribute[] {
                cpAttribute
            };
        }
        // create the access rule to restrict access to classes in
        // com.android.internal
        IAccessRule accessRule = JavaCore.newAccessRule(new Path("com/android/internal/**"), //$NON-NLS-1$
                IAccessRule.K_NON_ACCESSIBLE);

        IClasspathEntry frameworkClasspathEntry = JavaCore.newLibraryEntry(androidLib,
                androidSrc, // source attachment path
                null, // default source attachment root path.
                new IAccessRule[] { accessRule },
                attributes,
                false // not exported.
                );

        list.add(frameworkClasspathEntry);

        // now deal with optional libraries
        if (paths.length >= 5) {
            String docPath = paths[CACHE_INDEX_OPT_DOCS_URI];
            int i = 4;
            while (i < paths.length) {
                Path jarPath = new Path(paths[i++]);

                attributes = null;
                if (docPath.length() > 0) {
                    attributes = new IClasspathAttribute[] {
                        JavaCore.newClasspathAttribute(
                                IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, docPath)
                    };
                }

                IClasspathEntry entry = JavaCore.newLibraryEntry(
                        jarPath,
                        null, // source attachment path
                        null, // default source attachment root path.
                        null,
                        attributes,
                        false // not exported.
                        );
                list.add(entry);
            }
        }

        if (apiURL != null) {
            ProjectHelper.saveStringProperty(root, PROPERTY_ANDROID_API, apiURL);
        }
        if (androidSrc != null && target != null) {
            ProjectHelper.saveStringProperty(root, getAndroidSourceProperty(target),
                    androidSrc.toOSString());
        }
        return list.toArray(new IClasspathEntry[list.size()]);
    }

    private static Bundle getSourceBundle() {
        String bundleId = System.getProperty(COM_ANDROID_IDE_ECLIPSE_ADT_SOURCE,
                COM_ANDROID_IDE_ECLIPSE_ADT_SOURCE);
        Bundle bundle = Platform.getBundle(bundleId);
        return bundle;
    }

    private static String getAndroidSourceProperty(IAndroidTarget target) {
        if (target == null) {
            return null;
        }
        String androidSourceProperty = PROPERTY_ANDROID_SOURCE + "_"
                + target.getVersion().getApiString();
        return androidSourceProperty;
    }

    private static boolean testURL(String androidApiURL) {
        boolean valid = false;
        InputStream is = null;
        try {
            URL testURL = new URL(androidApiURL);
            is = testURL.openStream();
            valid = true;
        } catch (Exception ignore) {
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException ignore) {
                }
            }
        }
        return valid;
    }

    /**
     * Checks the projects' caches. If the cache was valid, the project is removed from the list.
     * @param projects the list of projects to check.
     */
    public static void checkProjectsCache(ArrayList<IJavaProject> projects) {
        Sdk currentSdk = Sdk.getCurrent();
        int i = 0;
        projectLoop: while (i < projects.size()) {
            IJavaProject javaProject = projects.get(i);
            IProject iProject = javaProject.getProject();

            // check if the project is opened
            if (iProject.isOpen() == false) {
                // remove from the list
                // we do not increment i in this case.
                projects.remove(i);

                continue;
            }

            // project that have been resolved before the sdk was loaded
            // will have a ProjectState where the IAndroidTarget is null
            // so we load the target now that the SDK is loaded.
            IAndroidTarget target = currentSdk.loadTarget(Sdk.getProjectState(iProject));
            if (target == null) {
                // this is really not supposed to happen. This would mean there are cached paths,
                // but project.properties was deleted. Keep the project in the list to force
                // a resolve which will display the error.
                i++;
                continue;
            }

            String[] targetPaths = getTargetPaths(target);

            // now get the cached paths
            String cache = ProjectHelper.loadStringProperty(iProject, PROPERTY_CONTAINER_CACHE);
            if (cache == null) {
                // this should not happen. We'll force resolve again anyway.
                i++;
                continue;
            }

            String[] cachedPaths = cache.split(Pattern.quote(PATH_SEPARATOR));
            if (cachedPaths.length < 3 || cachedPaths.length == 4) {
                // paths length is wrong. simply resolve the project again
                i++;
                continue;
            }

            // Now we compare the paths. The first 4 can be compared directly.
            // because of case sensitiveness we need to use File objects

            if (targetPaths.length != cachedPaths.length) {
                // different paths, force resolve again.
                i++;
                continue;
            }

            // compare the main paths (android.jar, main sources, main javadoc)
            if (new File(targetPaths[CACHE_INDEX_JAR]).equals(
                            new File(cachedPaths[CACHE_INDEX_JAR])) == false ||
                    new File(targetPaths[CACHE_INDEX_SRC]).equals(
                            new File(cachedPaths[CACHE_INDEX_SRC])) == false ||
                    new File(targetPaths[CACHE_INDEX_DOCS_URI]).equals(
                            new File(cachedPaths[CACHE_INDEX_DOCS_URI])) == false) {
                // different paths, force resolve again.
                i++;
                continue;
            }

            if (cachedPaths.length > CACHE_INDEX_OPT_DOCS_URI) {
                // compare optional libraries javadoc
                if (new File(targetPaths[CACHE_INDEX_OPT_DOCS_URI]).equals(
                        new File(cachedPaths[CACHE_INDEX_OPT_DOCS_URI])) == false) {
                    // different paths, force resolve again.
                    i++;
                    continue;
                }

                // testing the optional jar files is a little bit trickier.
                // The order is not guaranteed to be identical.
                // From a previous test, we do know however that there is the same number.
                // The number of libraries should be low enough that we can simply go through the
                // lists manually.
                targetLoop: for (int tpi = 4 ; tpi < targetPaths.length; tpi++) {
                    String targetPath = targetPaths[tpi];

                    // look for a match in the other array
                    for (int cpi = 4 ; cpi < cachedPaths.length; cpi++) {
                        if (new File(targetPath).equals(new File(cachedPaths[cpi]))) {
                            // found a match. Try the next targetPath
                            continue targetLoop;
                        }
                    }

                    // if we stop here, we haven't found a match, which means there's a
                    // discrepancy in the libraries. We force a resolve.
                    i++;
                    continue projectLoop;
                }
            }

            // at the point the check passes, and we can remove the project from the list.
            // we do not increment i in this case.
            projects.remove(i);
        }
    }

    /**
     * Returns the paths necessary to create the {@link IClasspathEntry} for this targets.
     * <p/>The paths are always in the same order.
     * <ul>
     * <li>Path to android.jar</li>
     * <li>Path to the source code for android.jar</li>
     * <li>Path to the javadoc for the android platform</li>
     * </ul>
     * Additionally, if there are optional libraries, the array will contain:
     * <ul>
     * <li>Path to the libraries javadoc</li>
     * <li>Path to the first .jar file</li>
     * <li>(more .jar as needed)</li>
     * </ul>
     */
    private static String[] getTargetPaths(IAndroidTarget target) {
        ArrayList<String> paths = new ArrayList<String>();

        // first, we get the path for android.jar
        // The order is: android.jar, source folder, docs folder
        paths.add(target.getPath(IAndroidTarget.ANDROID_JAR));
        paths.add(target.getPath(IAndroidTarget.SOURCES));
        paths.add(AdtPlugin.getUrlDoc());

        // now deal with optional libraries.
        IOptionalLibrary[] libraries = target.getOptionalLibraries();
        if (libraries != null) {
            // all the optional libraries use the same javadoc, so we start with this
            String targetDocPath = target.getPath(IAndroidTarget.DOCS);
            if (targetDocPath != null) {
                paths.add(ProjectHelper.getJavaDocPath(targetDocPath));
            } else {
                // we add an empty string, to always have the same count.
                paths.add("");
            }

            // because different libraries could use the same jar file, we make sure we add
            // each jar file only once.
            HashSet<String> visitedJars = new HashSet<String>();
            for (IOptionalLibrary library : libraries) {
                String jarPath = library.getJarPath();
                if (visitedJars.contains(jarPath) == false) {
                    visitedJars.add(jarPath);
                    paths.add(jarPath);
                }
            }
        }

        return paths.toArray(new String[paths.size()]);
    }

    @Override
    public boolean canUpdateClasspathContainer(IPath containerPath, IJavaProject project) {
        return true;
    }

    @Override
    public void requestClasspathContainerUpdate(IPath containerPath, IJavaProject project,
            IClasspathContainer containerSuggestion) throws CoreException {
        AdtPlugin plugin = AdtPlugin.getDefault();

        synchronized (Sdk.getLock()) {
            boolean sdkIsLoaded = plugin.getSdkLoadStatus() == LoadStatus.LOADED;

            // check if the project has a valid target.
            IAndroidTarget target = null;
            if (sdkIsLoaded) {
                target = Sdk.getCurrent().getTarget(project.getProject());
            }
            if (sdkIsLoaded && target != null) {
                String[] paths = getTargetPaths(target);
                IPath android_lib = new Path(paths[CACHE_INDEX_JAR]);
                IClasspathEntry[] entries = containerSuggestion.getClasspathEntries();
                for (int i = 0; i < entries.length; i++) {
                    IClasspathEntry entry = entries[i];
                    if (entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) {
                        IPath entryPath = entry.getPath();

                        if (entryPath != null) {
                            if (entryPath.equals(android_lib)) {
                                IPath entrySrcPath = entry.getSourceAttachmentPath();
                                IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
                                if (entrySrcPath != null) {
                                    ProjectHelper.saveStringProperty(root,
                                            getAndroidSourceProperty(target),
                                            entrySrcPath.toString());
                                } else {
                                    ProjectHelper.saveStringProperty(root,
                                            getAndroidSourceProperty(target), null);
                                }
                                IClasspathAttribute[] extraAttributtes = entry.getExtraAttributes();
                                if (extraAttributtes.length == 0) {
                                    ProjectHelper.saveStringProperty(root, PROPERTY_ANDROID_API,
                                            NULL_API_URL);
                                }
                                for (int j = 0; j < extraAttributtes.length; j++) {
                                    IClasspathAttribute extraAttribute = extraAttributtes[j];
                                    String value = extraAttribute.getValue();
                                    if ((value == null || value.trim().length() == 0)
                                            && IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME
                                                    .equals(extraAttribute.getName())) {
                                        value = NULL_API_URL;
                                    }
                                    if (IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME
                                            .equals(extraAttribute.getName())) {
                                        ProjectHelper.saveStringProperty(root,
                                                PROPERTY_ANDROID_API, value);

                                    }
                                }
                            }
                        }
                    }
                }
                rebindClasspathEntries(project.getJavaModel(), containerPath);
            }
        }
    }

    private static void rebindClasspathEntries(IJavaModel model, IPath containerPath)
            throws JavaModelException {
        ArrayList<IJavaProject> affectedProjects = new ArrayList<IJavaProject>();

        IJavaProject[] projects = model.getJavaProjects();
        for (int i = 0; i < projects.length; i++) {
            IJavaProject project = projects[i];
            IClasspathEntry[] entries = project.getRawClasspath();
            for (int k = 0; k < entries.length; k++) {
                IClasspathEntry curr = entries[k];
                if (curr.getEntryKind() == IClasspathEntry.CPE_CONTAINER
                        && containerPath.equals(curr.getPath())) {
                    affectedProjects.add(project);
                }
            }
        }
        if (!affectedProjects.isEmpty()) {
            IJavaProject[] affected = affectedProjects
                    .toArray(new IJavaProject[affectedProjects.size()]);
            updateProjects(affected);
        }
    }

}
