/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
 *
 * 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 org.netbeans.modules.debugger.jpda.projects;

import java.awt.EventQueue;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.netbeans.api.debugger.Properties;
import org.netbeans.api.debugger.jpda.JPDADebugger;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.java.classpath.ClassPath.Entry;
import org.netbeans.api.java.classpath.GlobalPathRegistry;
import org.netbeans.api.java.classpath.GlobalPathRegistryEvent;
import org.netbeans.api.java.classpath.GlobalPathRegistryListener;
import org.netbeans.api.java.platform.JavaPlatform;
import org.netbeans.api.java.platform.JavaPlatformManager;
import org.netbeans.api.java.project.JavaProjectConstants;
import org.netbeans.api.java.queries.SourceForBinaryQuery;
import org.netbeans.api.java.source.BuildArtifactMapper;
import org.netbeans.api.java.source.BuildArtifactMapper.ArtifactsUpdated;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.api.project.SourceGroup;
import org.netbeans.api.project.ui.OpenProjects;
import org.netbeans.spi.debugger.ContextProvider;
import org.netbeans.spi.debugger.jpda.SourcePathProvider;
import org.netbeans.spi.java.classpath.PathResourceImplementation;
import org.netbeans.spi.java.classpath.support.ClassPathSupport;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileStateInvalidException;
import org.openide.filesystems.FileUtil;
import org.openide.filesystems.JarFileSystem;
import org.openide.filesystems.URLMapper;
import org.openide.util.BaseUtilities;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import org.openide.util.WeakListeners;


/**
 *
 * @author Jan Jancura
 */
@SourcePathProvider.Registration(path="netbeans-JPDASession")
public class SourcePathProviderImpl extends SourcePathProvider {
    
    private static final boolean    verbose =
        System.getProperty ("netbeans.debugger.sourcepathproviderimpl") != null;
    
    private static final Logger logger = Logger.getLogger("org.netbeans.modules.debugger.jpda.projects");
    
    private static final Pattern thisDirectoryPattern = Pattern.compile("(/|\\A)\\./");
    private static final Pattern parentDirectoryPattern = Pattern.compile("(/|\\A)([^/]+?)/\\.\\./");

    /** Contains all known source paths + jdk source path for JPDAStart task + {@link #additionalSourceRoots} */
    private ClassPath               originalSourcePath;
    /** Contains the additional source roots, added at a later time to the original roots. */
    private Set<String>             additionalSourceRoots;
    /** Contains platform (JDK) source roots. */
    private Set<String>             platformSourceRoots;
    /** Contains just the source paths from {@link #originalSourcePath} which are selected for debugging. */
    private ClassPath               smartSteppingSourcePath;
    /** Roots of {@link #originalSourcePath} */
    private String[]                projectSourceRoots;
    /** {@link #originalSourcePath} in the original order. */
    private ClassPath               unorderedOriginalSourcePath;
    /** Permutation that creates {@link #originalSourcePath} from {@link #unorderedOriginalSourcePath}. */
    private int[]                   sourcePathPermutation;
    private PropertyChangeSupport   pcs;
    private PathRegistryListener    pathRegistryListener;
    private File                    baseDir;

    private final Map<String, String> urlCache = new URLCacheMap();
    private final Map<String, String> urlCacheGlobal = new URLCacheMap();
    
    public SourcePathProviderImpl () {
        pcs = new PropertyChangeSupport (this);
    }

    public SourcePathProviderImpl (ContextProvider contextProvider) {
        pcs = new PropertyChangeSupport (this);
        //this.session = (Session) contextProvider.lookupFirst 
        //    (null, Session.class);
        JPDADebugger debugger = (JPDADebugger) contextProvider.lookupFirst(null, JPDADebugger.class);
        Map properties = contextProvider.lookupFirst(null, Map.class);

        Set<FileObject> srcRootsToListenForArtifactsUpdates = null;
        
        // 2) get default allSourceRoots of source roots used for stepping
        if (logger.isLoggable(Level.FINE)) logger.fine("Have properties = "+properties);
        if (properties != null) {
            baseDir = (File) properties.get("baseDir");
            smartSteppingSourcePath = (ClassPath) properties.get ("sourcepath");
            ClassPath jdkCP = (ClassPath) properties.get ("jdksources");
            if ( (jdkCP == null) && (JavaPlatform.getDefault () != null) ) {
                jdkCP = JavaPlatform.getDefault ().getSourceFolders ();
            }
            platformSourceRoots = getSourceRootsSet(jdkCP);
            ClassPath additionalClassPath;
            if (baseDir != null) {
                additionalClassPath = getAdditionalClassPath(baseDir);
            } else {
                additionalClassPath = null;
                Exceptions.printStackTrace(new NullPointerException("No base directory is defined. Properties = "+properties));
            }
            if (additionalClassPath != null) {
                smartSteppingSourcePath = ClassPathSupport.createProxyClassPath (
                        new ClassPath[] {
                            smartSteppingSourcePath,
                            additionalClassPath
                        });
            }
            smartSteppingSourcePath = jdkCP == null ?
                smartSteppingSourcePath :
                ClassPathSupport.createProxyClassPath (
                    new ClassPath[] {
                        jdkCP,
                        smartSteppingSourcePath,
                    }
            );
            unorderedOriginalSourcePath = smartSteppingSourcePath;

            Map<String, Integer> orderIndexes = getSourceRootsOrder(baseDir);
            String[] unorderedOriginalRoots = getSourceRoots(unorderedOriginalSourcePath);
            String[] sortedOriginalRoots = new String[unorderedOriginalRoots.length];
            sourcePathPermutation = createPermutation(unorderedOriginalRoots,
                                                      orderIndexes,
                                                      sortedOriginalRoots);
            smartSteppingSourcePath = createClassPath(sortedOriginalRoots);
            
            originalSourcePath = smartSteppingSourcePath;

            Set<String> disabledRoots;
            if (baseDir != null) {
                disabledRoots = getDisabledSourceRoots(baseDir);
            } else {
                disabledRoots = null;
            }
            if (disabledRoots != null && !disabledRoots.isEmpty()) {
                List<FileObject> enabledSourcePath = new ArrayList<FileObject>(
                        Arrays.asList(smartSteppingSourcePath.getRoots()));
                for (FileObject fo : new HashSet<FileObject>(enabledSourcePath)) {
                    if (disabledRoots.contains(getRoot(fo))) {
                        enabledSourcePath.remove(fo);
                    }
                }
                smartSteppingSourcePath = createClassPath(
                        enabledSourcePath.toArray(new FileObject[0]));
            }

            projectSourceRoots = getSourceRoots(originalSourcePath);
            //Set<FileObject> preferredRoots = new HashSet<FileObject>();
            //preferredRoots.addAll(Arrays.asList(originalSourcePath.getRoots()));
            /*
            Set<FileObject> globalRoots = new TreeSet<FileObject>(new FileObjectComparator());
            globalRoots.addAll(GlobalPathRegistry.getDefault().getSourceRoots());
            globalRoots.removeAll(preferredRoots);
            ClassPath globalCP = createClassPath(globalRoots.toArray(new FileObject[0]));
            originalSourcePath = ClassPathSupport.createProxyClassPath(
                    originalSourcePath,
                    globalCP
            );
             */
            String listeningCP = (String) properties.get("listeningCP");
            if (listeningCP != null) {
                boolean isSourcepath = false;
                if ("sourcepath".equalsIgnoreCase(listeningCP)) {
                    listeningCP = ((ClassPath) properties.get ("sourcepath")).toString(ClassPath.PathConversionMode.SKIP);
                    isSourcepath = true;
                }
                srcRootsToListenForArtifactsUpdates = new HashSet<FileObject>();
                for (String cp : listeningCP.split(File.pathSeparator)) {
                    logger.log(Level.FINE, "Listening cp = ''{0}''", cp);
                    File f = new File(cp);
                    f = FileUtil.normalizeFile(f);
                    URL entry = FileUtil.urlForArchiveOrDir(f);

                    if (entry != null) {
                        if (isSourcepath) {
                            FileObject src = URLMapper.findFileObject(entry);
                            if (src != null) {
                                srcRootsToListenForArtifactsUpdates.add(src);
                            }
                        }
                        for (FileObject src : SourceForBinaryQuery.findSourceRoots(entry).getRoots()) {
                            srcRootsToListenForArtifactsUpdates.add(src);
                        }
                    }
                }
                if (srcRootsToListenForArtifactsUpdates.isEmpty()) {
                    srcRootsToListenForArtifactsUpdates = null;
                }
            }
        } else {
            pathRegistryListener = new PathRegistryListener();
            GlobalPathRegistry.getDefault().addGlobalPathRegistryListener(
                    WeakListeners.create(GlobalPathRegistryListener.class,
                                         pathRegistryListener,
                                         GlobalPathRegistry.getDefault()));
            JavaPlatformManager.getDefault ().addPropertyChangeListener(
                    WeakListeners.propertyChange(pathRegistryListener,
                                                 JavaPlatformManager.getDefault()));
            
            List<FileObject> allSourceRoots = new ArrayList<FileObject>();
            Set<FileObject> preferredRoots = new HashSet<FileObject>();
            Set<FileObject> addedBinaryRoots = new HashSet<FileObject>();
            Project mainProject = OpenProjects.getDefault().getMainProject();
            platformSourceRoots = new HashSet<String>();
            if (mainProject != null) {
                SourceGroup[] sgs = ProjectUtils.getSources(mainProject).getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA);
                for (SourceGroup sg : sgs) {
                    ClassPath ecp = ClassPath.getClassPath(sg.getRootFolder(), ClassPath.EXECUTE);
                    if (ecp == null) {
                        ecp = ClassPath.getClassPath(sg.getRootFolder(), ClassPath.SOURCE);
                    }
                    if (ecp != null) {
                        FileObject[] binaryRoots = ecp.getRoots();
                        for (FileObject fo : binaryRoots) {
                            if (addedBinaryRoots.contains(fo)) {
                                continue;
                            }
                            addedBinaryRoots.add(fo);
                            FileObject[] roots = SourceForBinaryQuery.findSourceRoots(fo.toURL()).getRoots();
                            for (FileObject fr : roots) {
                                if (!preferredRoots.contains(fr)) {
                                    allSourceRoots.add(fr);
                                    preferredRoots.add(fr);
                                }
                            }
                        }
                    }
                    ecp = ClassPath.getClassPath(sg.getRootFolder(), ClassPath.BOOT);
                    if (ecp != null) {
                        platformSourceRoots.addAll(getSourceRootsSet(ecp));
                    }
                }
            }
            if (logger.isLoggable(Level.FINE)) {
                logger.fine("SourcePathProviderImpl: preferred source roots = "+preferredRoots+")");
            }
            Set<FileObject> globalRoots = new TreeSet<FileObject>(new FileObjectComparator());
            globalRoots.addAll(GlobalPathRegistry.getDefault().getSourceRoots());
            for (FileObject fo : globalRoots) {
                if (!preferredRoots.contains(fo)) {
                    allSourceRoots.add(fo);
                }
            }
            // TODO: Add first main project's BOOT path, if not exist, then default platform and then the rest.
            JavaPlatform[] platforms = JavaPlatformManager.getDefault().getInstalledPlatforms();
            for (int i = 0; i < platforms.length; i++) {
                FileObject[] roots = platforms[i].getSourceFolders().getRoots ();
                int j, jj = roots.length;
                for (j = 0; j < jj; j++) {
                    if (!allSourceRoots.contains(roots [j])) {
                        allSourceRoots.add(roots [j]);
                    }
                }
                platformSourceRoots.addAll(getSourceRootsSet(platforms[i].getSourceFolders()));
            }
            List<FileObject> additional = getAdditionalRemoteClassPath();
            if (additional != null) {
                allSourceRoots.addAll(additional);
            }
            if (logger.isLoggable(Level.FINE)) {
                logger.fine("SourcePathProviderImpl: GlobalPathRegistry roots = "+GlobalPathRegistry.getDefault().getSourceRoots()+")");
                logger.fine("Platform roots:");
                for (int i = 0; i < platforms.length; i++) {
                    logger.fine(" "+Arrays.asList(platforms[i].getSourceFolders().getRoots ()).toString());
                }
                logger.fine("SourcePathProviderImpl: all source roots = "+allSourceRoots+")");
            }

            Set<String> disabledRoots = getRemoteDisabledSourceRoots();
            
            synchronized (this) {
                unorderedOriginalSourcePath = createClassPath (
                    allSourceRoots.toArray
                        (new FileObject [allSourceRoots.size()])
                );
                
                Map<String, Integer> orderIndexes = getRemoteSourceRootsOrder();
                String[] unorderedOriginalRoots = getSourceRoots(unorderedOriginalSourcePath);
                String[] sorterOriginalRoots = new String[unorderedOriginalRoots.length];
                sourcePathPermutation = createPermutation(unorderedOriginalRoots,
                                                          orderIndexes,
                                                          sorterOriginalRoots);
                originalSourcePath = createClassPath(sorterOriginalRoots);

                projectSourceRoots = getSourceRoots(originalSourcePath);

                srcRootsToListenForArtifactsUpdates = new HashSet<FileObject>(allSourceRoots);

                smartSteppingSourcePath = originalSourcePath;

                if (disabledRoots != null && !disabledRoots.isEmpty()) {
                    List<FileObject> enabledSourcePath = new ArrayList<FileObject>(
                            Arrays.asList(smartSteppingSourcePath.getRoots()));
                    for (FileObject fo : new HashSet<FileObject>(enabledSourcePath)) {
                        if (disabledRoots.contains(getRoot(fo))) {
                            enabledSourcePath.remove(fo);
                        }
                    }
                    smartSteppingSourcePath = createClassPath(
                            enabledSourcePath.toArray(new FileObject[0]));
                }
            }
        }
        
        if (verbose) 
            System.out.println 
                ("SPPI: init originalSourcePath " + originalSourcePath);    
        if (verbose) 
            System.out.println (
                "SPPI: init smartSteppingSourcePath " + smartSteppingSourcePath
            );

        if (logger.isLoggable(Level.FINE)) {
            logger.fine("new SourcePathProviderImpl(): contextProvider = "+contextProvider+
                        ", properties = "+properties+
                        ", srcRootsToListenForArtifactsUpdates = "+srcRootsToListenForArtifactsUpdates);
        }

        if (srcRootsToListenForArtifactsUpdates != null) {
            final Set<ArtifactsUpdatedImpl> artifactsListeners = new HashSet<ArtifactsUpdatedImpl>();
            for (FileObject src : srcRootsToListenForArtifactsUpdates) {
                artifactsListeners.add(addArtifactsUpdateListenerFor(debugger, src));
            }
            debugger.addPropertyChangeListener(JPDADebugger.PROP_STATE, new PropertyChangeListener() {
                @Override
                public void propertyChange(PropertyChangeEvent evt) {
                    if (JPDADebugger.STATE_DISCONNECTED == ((Integer) evt.getNewValue()).intValue()) {
                        for (ArtifactsUpdatedImpl al : artifactsListeners) {
                            BuildArtifactMapper.removeArtifactsUpdatedListener(al.getURL(), al);
                        }
                    }
                }
            });
        }
    }

    private ClassPath getAdditionalClassPath(File baseDir) {
        try {
            String root = BaseUtilities.toURI(baseDir).toURL().toExternalForm();
            Properties sourcesProperties = Properties.getDefault ().getProperties ("debugger").getProperties ("sources");
            List<String> additionalSourceRoots = (List<String>) sourcesProperties.
                    getProperties("additional_source_roots").
                    getMap("project", Collections.emptyMap()).
                    get(root);
            if (additionalSourceRoots == null || additionalSourceRoots.isEmpty()) {
                return null;
            }
            List<FileObject> additionalSourcePath = new ArrayList<FileObject>(additionalSourceRoots.size());
            for (String ar : additionalSourceRoots) {
                FileObject fo = getFileObject(ar);
                if (fo != null && fo.canRead()) {
                    additionalSourcePath.add(fo);
                }
            }
            this.additionalSourceRoots = new LinkedHashSet<String>(additionalSourceRoots);
            return createClassPath(
                    additionalSourcePath.toArray(new FileObject[0]));
        } catch (MalformedURLException ex) {
            Exceptions.printStackTrace(ex);
            return null;
        }
    }

    private List<FileObject> getAdditionalRemoteClassPath() {
        Properties sourcesProperties = Properties.getDefault ().getProperties ("debugger").getProperties ("sources");
        List<String> additionalSourceRoots = (List<String>) sourcesProperties.
                getProperties("additional_source_roots").
                getCollection("src_roots", Collections.emptyList());
        if (additionalSourceRoots == null || additionalSourceRoots.isEmpty()) {
            return null;
        }
        List<FileObject> additionalSourcePath = new ArrayList<FileObject>(additionalSourceRoots.size());
        for (String ar : additionalSourceRoots) {
            FileObject fo = getFileObject(ar);
            if (fo != null && fo.canRead()) {
                additionalSourcePath.add(fo);
            }
        }
        this.additionalSourceRoots = new LinkedHashSet<String>(additionalSourceRoots);
        return additionalSourcePath;
        //return ClassPathSupport.createClassPath(
        //        additionalSourcePath.toArray(new FileObject[0]));
    }

    private void storeAdditionalSourceRoots() {
        Properties sourcesProperties = Properties.getDefault ().getProperties ("debugger").getProperties ("sources");
        if (baseDir != null) {
            String projectRoot;
            try {
                projectRoot = BaseUtilities.toURI(baseDir).toURL().toExternalForm();
            } catch (MalformedURLException ex) {
                Exceptions.printStackTrace(ex);
                return ;
            }
            Map map = sourcesProperties.getProperties("additional_source_roots").
                getMap("project", new HashMap());
            if (additionalSourceRoots != null) {
                map.put(projectRoot, new ArrayList<String>(additionalSourceRoots));
            } else {
                map.remove(projectRoot);
            }
            sourcesProperties.getProperties("additional_source_roots").
                    setMap("project", map);
        } else {
            if (additionalSourceRoots != null) {
                sourcesProperties.getProperties("additional_source_roots").
                        setCollection("src_roots", new ArrayList<String>(additionalSourceRoots));
            } else {
                sourcesProperties.getProperties("additional_source_roots").
                        setCollection("src_roots", null);
            }
        }
    }

    private Set<String> getDisabledSourceRoots(File baseDir) {
        try {
            String root = BaseUtilities.toURI(baseDir).toURL().toExternalForm();
            Properties sourcesProperties = Properties.getDefault ().getProperties ("debugger").getProperties ("sources");
            return (Set<String>) sourcesProperties.getProperties("source_roots").
                getMap("project_disabled", Collections.emptyMap()).
                get(root);
        } catch (MalformedURLException ex) {
            Exceptions.printStackTrace(ex);
            return null;
        }
    }

    private Set<String> getRemoteDisabledSourceRoots() {
        Properties sourcesProperties = Properties.getDefault ().getProperties ("debugger").getProperties ("sources");
        return (Set<String>) sourcesProperties.getProperties("source_roots").
            getCollection("remote_disabled", Collections.emptySet());
    }

    private void storeDisabledSourceRoots(Set<String> disabledSourceRoots) {
        Properties sourcesProperties = Properties.getDefault ().getProperties ("debugger").getProperties ("sources");
        if (baseDir != null) {
            String projectRoot;
            try {
                projectRoot = BaseUtilities.toURI(baseDir).toURL().toExternalForm();
            } catch (MalformedURLException ex) {
                Exceptions.printStackTrace(ex);
                return ;
            }
            Map map = sourcesProperties.getProperties("source_roots").
                    getMap("project_disabled", new HashMap());
            map.put(projectRoot, disabledSourceRoots);
            sourcesProperties.getProperties("source_roots").
                    setMap("project_disabled", map);
        } else {
            sourcesProperties.getProperties("source_roots").
                    setCollection("remote_disabled", disabledSourceRoots);
        }
    }

    private static Map<String, Integer> getSourceRootsOrder(File baseDir) {
        try {
            String root = BaseUtilities.toURI(baseDir).toURL().toExternalForm();
            return getSourceRootsOrder(root);
        } catch (MalformedURLException ex) {
            Exceptions.printStackTrace(ex);
            return null;
        }
    }

    public static Map<String, Integer> getSourceRootsOrder(String root) {
        Properties sourcesProperties = Properties.getDefault ().getProperties ("debugger").getProperties ("sources");
        return (Map<String, Integer>) sourcesProperties.getProperties("source_roots").
            getMap("project_order", Collections.emptyMap()).
            get(root);
    }

    public static Map<String, Integer> getRemoteSourceRootsOrder() {
        Properties sourcesProperties = Properties.getDefault ().getProperties ("debugger").getProperties ("sources");
        return (Map<String, Integer>) sourcesProperties.getProperties("source_roots").
            getMap("remote_order", Collections.emptyMap());
    }

    private static void storeSourceRootsOrder(File baseDir, String[] roots, int[] permutation) {
        String projectRoot;
        if (baseDir != null) {
            try {
                projectRoot = BaseUtilities.toURI(baseDir).toURL().toExternalForm();
            } catch (MalformedURLException ex) {
                Exceptions.printStackTrace(ex);
                return ;
            }
        } else {
            projectRoot = null;
        }
        storeSourceRootsOrder(projectRoot, roots, permutation);
    }

    public static void storeSourceRootsOrder(String projectRoot, String[] roots, int[] permutation) {
        Map<String, Integer> sourceOrder = new HashMap<String, Integer>();
        if (roots.length != permutation.length) {
            throw new IllegalArgumentException("Incompatible array length: roots = "+roots.length+", permutation = "+permutation.length);
        }
        for (int i = 0; i < roots.length; i++) {
            sourceOrder.put(roots[permutation[i]], i);
        }
        if (logger.isLoggable(Level.FINE)) {
            logger.fine("SourcePathProviderImpl.storeSourceRootsOrder():");
            logger.fine("  sourceOrder = "+sourceOrder);
        }
        storeSourceRootsOrder(projectRoot, sourceOrder);
    }

    private static void storeSourceRootsOrder(String projectRoot, Map<String, Integer> sourceOrder) {
        Properties sourcesProperties = Properties.getDefault ().getProperties ("debugger").getProperties ("sources");
        if (projectRoot != null) {
            Map map = sourcesProperties.getProperties("source_roots").
                    getMap("project_order", new HashMap());
            map.put(projectRoot, sourceOrder);
            sourcesProperties.getProperties("source_roots").
                    setMap("project_order", map);
        } else {
            sourcesProperties.getProperties("source_roots").
                    setMap("remote_order", sourceOrder);
        }
    }

    /**
     * Translates a relative path ("java/lang/Thread.java") to url 
     * ("file:///C:/Sources/java/lang/Thread.java"). Uses GlobalPathRegistry
     * if global == true.
     *
     * @param relativePath a relative path (java/lang/Thread.java)
     * @param global true if global path should be used
     * @return url or <code>null</code>
     */
    @Override
    public String getURL (String relativePath, boolean global) {    if (verbose) System.out.println ("SPPI: getURL " + relativePath + " global " + global);
        relativePath = normalize(relativePath);
        if (global) {
            synchronized (urlCacheGlobal) {
                if (urlCacheGlobal.containsKey(relativePath)) {
                    if (verbose) System.out.println("Have cached global path for '"+relativePath+"' url = "+urlCacheGlobal.get(relativePath));
                    return urlCacheGlobal.get(relativePath);    // URL or null
                }
            }
        } else {
            synchronized (urlCache) {
                if (urlCache.containsKey(relativePath)) {
                    if (verbose) System.out.println("Have cached path for '"+relativePath+"' url = "+urlCache.get(relativePath));
                    return urlCache.get(relativePath);  // URL or null
                }
            }
        }
        FileObject fo;
        ClassPath ss = null;
        ClassPath os = null;
        synchronized (this) {
            if (originalSourcePath != null) {
                ss = smartSteppingSourcePath;
                os = originalSourcePath;
            }
        }
        if (ss != null && os != null) {
            fo = ss.findResource(relativePath);
            if (fo == null && global) {
                fo = os.findResource(relativePath);
            }
            if (fo == null && global) {
                fo = GlobalPathRegistry.getDefault().findResource(relativePath);
            }
        } else {
            fo = GlobalPathRegistry.getDefault().findResource(relativePath);
        }
        if (fo == null && global) {
            Set<ClassPath> cpaths = GlobalPathRegistry.getDefault().getPaths(ClassPath.COMPILE);
            for (ClassPath cp : cpaths) {
                fo = cp.findResource(relativePath);
                if (fo != null) {
                    FileObject[] roots = cp.getRoots();
                    for (FileObject r : roots) {
                        if (FileUtil.isParentOf(r, fo)) {
                            addToSourcePath(r, false);
                            break;
                        }
                    }
                    break;
                }
            }
        }
        
        if (verbose) System.out.println ("SPPI:   fo " + fo);

        String url;
        if (fo == null) {
            url = null;
        } else {
            url = fo.toURL ().toString ();
        }
        if (global) {
            synchronized (urlCacheGlobal) {
                if (verbose) System.out.println("Storing path into global cache for '"+relativePath+"' url = "+url);
                urlCacheGlobal.put(relativePath, url);
                if (verbose) System.out.println("  Global cache ("+urlCacheGlobal.size()+") = "+urlCacheGlobal);
            }
        } else {
            synchronized (urlCache) {
                if (verbose) System.out.println("Storing path into cache for '"+relativePath+"' url = "+url);
                urlCache.put(relativePath, url);
                if (verbose) System.out.println("  Cache = ("+urlCache.size()+") "+urlCache);
            }
        }
        return url;
    }
    
    private void addToSourcePath(FileObject sourceRoot, boolean clearURLCaches) {
        URL newURL = sourceRoot.toURL();
        synchronized (SourcePathProviderImpl.this) {
            if (originalSourcePath == null) {
                return ;
            }
            List<URL> sourcePaths = getURLRoots(originalSourcePath);
            sourcePaths.add(newURL);
            originalSourcePath =
                    SourcePathProviderImpl.createClassPath(
                        sourcePaths.toArray(new URL[0]));

            sourcePaths = getURLRoots(smartSteppingSourcePath);
            sourcePaths.add(newURL);
            smartSteppingSourcePath =
                    SourcePathProviderImpl.createClassPath(
                        sourcePaths.toArray(new URL[0]));
        }
        if (clearURLCaches) {
            synchronized (urlCache) {
                urlCache.clear();
            }
            synchronized (urlCacheGlobal) {
                urlCacheGlobal.clear();
            }
        }
        pcs.firePropertyChange (PROP_SOURCE_ROOTS, null, null);
    }
    
    /**
     * Translates a relative path to all possible URLs.
     * Uses GlobalPathRegistry if global == true.
     *
     * @param relativePath a relative path (java/lang/Thread.java)
     * @param global true if global path should be used
     * @return url
     */
    public String[] getAllURLs (String relativePath, boolean global) {      if (verbose) System.out.println ("SPPI: getURL " + relativePath + " global " + global);
        List<FileObject> fos;
        relativePath = normalize(relativePath);
        if (originalSourcePath == null) {
            fos = new ArrayList<FileObject>();
            for (ClassPath cp : GlobalPathRegistry.getDefault().getPaths(ClassPath.SOURCE)) {
                fos.addAll(cp.findAllResources(relativePath));
            }
        } else {
            synchronized (this) {
                if (!global) {
                    fos = smartSteppingSourcePath.findAllResources(relativePath);
                                                                            if (verbose) System.out.println ("SPPI:   fos " + fos);
                } else {
                    fos = originalSourcePath.findAllResources(relativePath);
                                                                            if (verbose) System.out.println ("SPPI:   fos " + fos);
                }
            }
        }
        List<String> urls = new ArrayList<String>(fos.size());
        for (FileObject fo : fos) {
            urls.add(fo.toURL().toString());
        }
        return urls.toArray(new String[0]);
    }
    
    /**
     * Returns relative path for given url.
     *
     * @param url a url of resource file
     * @param directorySeparator a directory separator character
     * @param includeExtension whether the file extension should be included 
     *        in the result
     *
     * @return relative path
     */
    @Override
    public String getRelativePath (
        String url, 
        char directorySeparator, 
        boolean includeExtension
    ) {
        // 1) url -> FileObject
        FileObject fo;                                              if (verbose) System.out.println ("SPPI: getRelativePath " + url);
        try {
            fo = URLMapper.findFileObject (new URL (url));          if (verbose) System.out.println ("SPPI:   fo " + fo);
        } catch (MalformedURLException e) {
            //e.printStackTrace ();
            return null;
        }
        if (fo == null) {
            return null;
        }
        String relativePath = smartSteppingSourcePath.getResourceName (
            fo, 
            directorySeparator,
            includeExtension
        );
        if (relativePath == null) {
            // fallback to FileObject's class path
            ClassPath cp = ClassPath.getClassPath (fo, ClassPath.SOURCE);
            if (cp == null) {
                cp = ClassPath.getClassPath (fo, ClassPath.COMPILE);
            }
            if (cp == null) {
                return null;
            }
            relativePath = cp.getResourceName (
                fo, 
                directorySeparator,
                includeExtension
            );
        }
        return relativePath;
    }
    
    /**
     * Returns the source root (if any) for given url.
     *
     * @param url a url of resource file
     *
     * @return the source root or <code>null</code> when no source root was found.
     */
    @Override
    public synchronized String getSourceRoot(String url) {
        FileObject fo;
        try {
            fo = URLMapper.findFileObject(new java.net.URL(url));
        } catch (java.net.MalformedURLException ex) {
            fo = null;
        }
        FileObject[] roots = null;
        if (fo != null && fo.canRead()) {
            ClassPath cp = ClassPath.getClassPath(fo, ClassPath.SOURCE);
            if (cp != null) {
                roots = cp.getRoots();
            }
        }
        if (roots == null) {
            roots = originalSourcePath.getRoots();
        }
        for (FileObject fileObject : roots) {
            String rootURL = fileObject.toURL().toString();
            if (url.startsWith(rootURL)) {
                String root = getRoot(fileObject);
                if (root != null) {
                    return root;
                }
            }
        }
        return null; // not found
    }

    private String[] getSourceRoots(ClassPath classPath) {
        FileObject[] sourceRoots = classPath.getRoots();
        List<String> roots = new ArrayList<String>(sourceRoots.length);
        for (FileObject fo : sourceRoots) {
            String root = getRoot(fo);
            if (root != null) {
                roots.add(root);
            }
        }
        return roots.toArray(new String[0]);
    }
    
    private static Set<String> getSourceRootsSet(ClassPath classPath) {
        FileObject[] sourceRoots = classPath.getRoots();
        Set<String> roots = new HashSet<String>(sourceRoots.length);
        for (FileObject fo : sourceRoots) {
            String root = getRoot(fo);
            if (root != null) {
                roots.add(root);
            }
        }
        return roots;
    }

    public synchronized Set<String> getPlatformSourceRoots() {
        return Collections.unmodifiableSet(platformSourceRoots);
    }

    /**
     * Returns allSourceRoots of original source roots.
     *
     * @return allSourceRoots of original source roots
     */
    @Override
    public synchronized String[] getOriginalSourceRoots () {
        return getSourceRoots(originalSourcePath);
    }
    
    /**
     * Returns array of source roots.
     *
     * @return array of source roots
     */
    @Override
    public synchronized String[] getSourceRoots () {
        return getSourceRoots(smartSteppingSourcePath);
    }

    public synchronized Set<FileObject> getSourceRootsFO() {
        return new HashSet<FileObject>(Arrays.asList(smartSteppingSourcePath.getRoots()));
    }
    
    /**
     * Returns the project's source roots.
     * 
     * @return array of source roots belonging to the project
     */
    public String[] getProjectSourceRoots() {
        return projectSourceRoots;
    }

    public synchronized String[] getAdditionalSourceRoots() {
        return (additionalSourceRoots == null) ? new String[] {} : additionalSourceRoots.toArray(new String[]{});
    }

    public void reorderOriginalSourceRoots(int[] permutation) {
        synchronized (this) {
        String[] srcRoots = getOriginalSourceRoots();
        if (permutation == null) {
            // Restting the order to the original
            for (int i = 0; i < sourcePathPermutation.length; i++) {
                sourcePathPermutation[i] = i;
            }
            originalSourcePath = unorderedOriginalSourcePath;
            srcRoots = getSourceRoots(unorderedOriginalSourcePath);
        } else {
            if (srcRoots.length != permutation.length) {
                throw new IllegalArgumentException("Bad length of permutation: "+permutation.length+", have "+srcRoots.length+" source roots.");
            }
            int n = permutation.length;
            String[] unorderedOriginalRoots = getSourceRoots(unorderedOriginalSourcePath);
            String[] sortedOriginalRoots = new String[n];
            // Adding the permutation
            for (int i = 0; i < n; i++) {
                permutation[i] = sourcePathPermutation[permutation[i]];
                sortedOriginalRoots[i] = unorderedOriginalRoots[permutation[i]];
            }
            System.arraycopy(permutation, 0, sourcePathPermutation, 0, n);
            originalSourcePath = createClassPath(sortedOriginalRoots);
            srcRoots = unorderedOriginalRoots;
        }
        projectSourceRoots = getSourceRoots(originalSourcePath);
        Set<String> smartSteppingRoots = new HashSet<String>(Arrays.asList(getSourceRoots(smartSteppingSourcePath)));
        String[] orderedSmartSteppingRoots = new String[smartSteppingRoots.size()];
        int i = 0;
        for (String root : projectSourceRoots) {
            if (smartSteppingRoots.contains(root)) {
                orderedSmartSteppingRoots[i++] = root;
            }
        }
        smartSteppingSourcePath = createClassPath(orderedSmartSteppingRoots);
        storeSourceRootsOrder(baseDir, srcRoots, sourcePathPermutation);
        }
        // Clear caches so that the new order is taken into account
        synchronized (urlCache) {
            urlCache.clear();
        }
        synchronized (urlCacheGlobal) {
            urlCacheGlobal.clear();
        }
    }
    
    /**
     * Sets array of source roots.
     * {@link #setSourceRoots(java.lang.String[])} can not save disabled additionalSourceRoots, since it gets
     * only *enabled* source roots
     *
     * @param sourceRoots a new array of sourceRoots
     * @param additionalRoots complete list of additional source roots (including disabled ones)
     */
    public void setSourceRoots (String[] sourceRoots, String[] additionalRoots) {
        if (logger.isLoggable(Level.FINE)) {
            logger.fine("SourcePathProviderImpl.setSourceRoots("+java.util.Arrays.asList(sourceRoots)+", "+java.util.Arrays.asList(additionalRoots)+")");
        }
        /*if (sourceRootRename(sourceRoots)) {
            return ;
        }*/
        Set<String> newRoots = new LinkedHashSet<String>(Arrays.asList(sourceRoots));
        ClassPath[] oldCP_ptr = new ClassPath[] { null };
        ClassPath[] newCP_ptr = new ClassPath[] { null };
        synchronized (this) {
            Set<String> allAdditionalSourceRoots = new LinkedHashSet<String>(Arrays.asList(additionalRoots));
            int permLength = sourcePathPermutation.length;
            Set<String> disabledSourceRoots = setSourceRoots(newRoots, oldCP_ptr, newCP_ptr, allAdditionalSourceRoots);

            storeAdditionalSourceRoots();
            storeDisabledSourceRoots(disabledSourceRoots);
            if (permLength != sourcePathPermutation.length) {
                storeSourceRootsOrder(baseDir, getSourceRoots(unorderedOriginalSourcePath), sourcePathPermutation);
            }
        }
        
        // Clear caches so that the new source roots are taken into account
        synchronized (urlCache) {
            urlCache.clear();
        }
        synchronized (urlCacheGlobal) {
            urlCacheGlobal.clear();
        }

        if (oldCP_ptr[0] != null) {
            pcs.firePropertyChange (PROP_SOURCE_ROOTS, oldCP_ptr[0], newCP_ptr[0]);
        }
    }
    
    /**
     * Sets array of source roots.
     *
     * @param sourceRoots a new array of sourceRoots
     */
    @Override
    public void setSourceRoots (String[] sourceRoots) {
        if (logger.isLoggable(Level.FINE)) {
            logger.fine("SourcePathProviderImpl.setSourceRoots("+java.util.Arrays.asList(sourceRoots)+")");
        }
        Set<String> newRoots = new LinkedHashSet<String>(Arrays.asList(sourceRoots));
        ClassPath[] oldCP_ptr = new ClassPath[] { null };
        ClassPath[] newCP_ptr = new ClassPath[] { null };
        synchronized (this) {
            int permLength = sourcePathPermutation.length;
            Set<String> disabledSourceRoots = setSourceRoots(newRoots, oldCP_ptr, newCP_ptr, null);
            
            storeAdditionalSourceRoots();
            storeDisabledSourceRoots(disabledSourceRoots);
            if (permLength != sourcePathPermutation.length) {
                storeSourceRootsOrder(baseDir, getSourceRoots(unorderedOriginalSourcePath), sourcePathPermutation);
            }
        }
        
        // Clear caches so that the new source roots are taken into account
        synchronized (urlCache) {
            urlCache.clear();
        }
        synchronized (urlCacheGlobal) {
            urlCacheGlobal.clear();
        }

        if (oldCP_ptr[0] != null) {
            pcs.firePropertyChange (PROP_SOURCE_ROOTS, oldCP_ptr[0], newCP_ptr[0]);
        }
    }

    private synchronized Set<String> setSourceRoots(Set<String> newRoots,
                                                    ClassPath[] oldCP_ptr,
                                                    ClassPath[] newCP_ptr,
                                                    Set<String> allAdditionalSourceRoots) {
        List<FileObject> sourcePath = new ArrayList<FileObject>(
                Arrays.asList(smartSteppingSourcePath.getRoots()));
        List<FileObject> sourcePathOriginal = new ArrayList<FileObject>(
                Arrays.asList(originalSourcePath.getRoots()));
        List<FileObject> unorderedSourcePathOriginal = new ArrayList<FileObject>(
                Arrays.asList(unorderedOriginalSourcePath.getRoots()));

        // First check whether there are some new source roots
        Set<String> newOriginalRoots = new LinkedHashSet<String>(newRoots);
        for (FileObject fo : sourcePathOriginal) {
            newOriginalRoots.remove(getRoot(fo));
        }
        if (!newOriginalRoots.isEmpty()) {
            // There are new additional source roots added. We need to update
            // unorderedOriginalSourcePath, originalSourcePath, projectSourceRoots,
            // smartSteppingSourcePath, sourcePathPermutation and additionalSourceRoots
            Set<String> addedOriginalRoots = new LinkedHashSet<String>(newOriginalRoots.size());
            for (String root : newOriginalRoots) {
                FileObject fo = getFileObject(root);
                if (fo != null && fo.canRead()) {
                    sourcePathOriginal.add(fo);
                    unorderedSourcePathOriginal.add(fo);
                    addedOriginalRoots.add(root);
                }
            }
            newOriginalRoots = addedOriginalRoots;
            if (!newOriginalRoots.isEmpty()) {
                if (additionalSourceRoots == null) {
                    additionalSourceRoots = new LinkedHashSet<String>();
                }
                additionalSourceRoots.addAll(newOriginalRoots);
            }
        }

        // Then correct the smart-stepping path
        Set<String> newSteppingRoots = new LinkedHashSet<String>(newRoots);
        for (FileObject fo : sourcePath) {
            newSteppingRoots.remove(getRoot(fo));
        }
        Set<FileObject> removedSteppingRoots = new HashSet<FileObject>();
        Set<FileObject> removedOriginalRoots = new HashSet<FileObject>();
        for (FileObject fo : sourcePath) {
            String spr = getRoot(fo);
            if (!newRoots.contains(spr)) {
                removedSteppingRoots.add(fo);
                if (additionalSourceRoots != null && additionalSourceRoots.contains(spr) &&
                    !(allAdditionalSourceRoots != null && allAdditionalSourceRoots.contains(spr))) {
                    // Remove it only if it's not among all additional source roots
                    removedOriginalRoots.add(fo);
                    additionalSourceRoots.remove(spr);
                    if (additionalSourceRoots.isEmpty()) {
                        additionalSourceRoots = null;
                    }
                }
            }
        }
        if (!removedOriginalRoots.isEmpty()) {
            sourcePathOriginal.removeAll(removedOriginalRoots);
        }
        if (!newOriginalRoots.isEmpty() || !removedOriginalRoots.isEmpty()) {
            for (FileObject fo : removedOriginalRoots) {
                int index = unorderedSourcePathOriginal.indexOf(fo);
                unorderedSourcePathOriginal.remove(index);
                int pi = sourcePathPermutation[index];
                for (int i = 0; i < sourcePathPermutation.length; i++) {
                    if (sourcePathPermutation[i] > pi) {
                        sourcePathPermutation[i]--;
                    }
                }
                for (int i = index; i < (sourcePathPermutation.length - 1); i++) {
                    sourcePathPermutation[i] = sourcePathPermutation[i+1];
                }
            }
            int n = sourcePathPermutation.length - removedOriginalRoots.size() + newOriginalRoots.size();
            int[] newSourcePathPermutation = new int[n];
            System.arraycopy(sourcePathPermutation, 0, newSourcePathPermutation, 0, sourcePathPermutation.length - removedOriginalRoots.size());
            for (int i = sourcePathPermutation.length - removedOriginalRoots.size(); i < n; i++) {
                newSourcePathPermutation[i] = i;
            }
            sourcePathPermutation = newSourcePathPermutation;
            originalSourcePath =
                    createClassPath(
                        sourcePathOriginal.toArray(new FileObject[0]));
            unorderedOriginalSourcePath =
                    createClassPath(
                        unorderedSourcePathOriginal.toArray(new FileObject[0]));
            projectSourceRoots = getSourceRoots(originalSourcePath);
        }
        if (newSteppingRoots.size() > 0 || removedSteppingRoots.size() > 0) {
            for (String root : newSteppingRoots) {
                FileObject fo = getFileObject(root);
                if (fo != null && fo.canRead()) {
                    sourcePath.add(fo);
                }
            }
            sourcePath.removeAll(removedSteppingRoots);
            oldCP_ptr[0] = smartSteppingSourcePath;
            smartSteppingSourcePath =
                    createClassPath(
                        sourcePath.toArray(new FileObject[0]));
            newCP_ptr[0] = smartSteppingSourcePath;
        }
        Set<FileObject> disabledRoots = new HashSet<FileObject>(sourcePathOriginal);
        disabledRoots.removeAll(sourcePath);
        Set<String> disabledSourceRoots = new HashSet<String>();
        for (FileObject fo : disabledRoots) {
            disabledSourceRoots.add(getRoot(fo));
        }
        return disabledSourceRoots;
    }

    /*
    private synchronized boolean sourceRootRename(String[] sourceRoots) {
        FileObject[] currentRoots = smartSteppingSourcePath.getRoots();
        if (currentRoots.length != sourceRoots.length) {
            return false;
        }
        int i = 0;
        int index = -1;
        String renamed = null;
        FileObject renamedFO = null;
        for (FileObject fo : currentRoots) {
            String root = getRoot(fo);
            if (root != null) {
                if (root.equals(sourceRoots[i])) {
                    if (index < 0) {
                        index = i;
                        renamed = root;
                        renamedFO = fo;
                    } else {
                        return false;
                    }
                }
            }
            i++;
        }
        String newRoot = sourceRoots[index];
        FileObject newFO = getFileObject(newRoot);
        if (newFO == null) {
            throw Exceptions.attachLocalizedMessage(new IllegalArgumentException(newRoot), newRoot+" does not exists.");
        }
        //currentRoots[index] = newRoot;
        additionalSourceRoots.remove(renamed);
        additionalSourceRoots.add(newRoot);
        List<FileObject> sourcePath = new ArrayList<FileObject>(
                Arrays.asList(smartSteppingSourcePath.getRoots()));
        List<FileObject> sourcePathOriginal = new ArrayList<FileObject>(
                Arrays.asList(originalSourcePath.getRoots()));
        List<FileObject> unorderedSourcePathOriginal = new ArrayList<FileObject>(
                Arrays.asList(unorderedOriginalSourcePath.getRoots()));
        sourcePath.set(index, newFO);
        index = sourcePathOriginal.indexOf(renamedFO);
        sourcePathOriginal.set(index, newFO);
        index = unorderedSourcePathOriginal.indexOf(renamedFO);
        unorderedSourcePathOriginal.set(index, newFO);
        smartSteppingSourcePath =
                createClassPath(
                    sourcePath.toArray(new FileObject[0]));
        originalSourcePath =
                createClassPath(
                    sourcePathOriginal.toArray(new FileObject[0]));
        unorderedOriginalSourcePath =
                createClassPath(
                    unorderedSourcePathOriginal.toArray(new FileObject[0]));
        projectSourceRoots = getSourceRoots(originalSourcePath);
        Set<String> disabledRoots;
        if (baseDir != null) {
            disabledRoots = getDisabledSourceRoots(baseDir);
        } else {
            disabledRoots = getRemoteDisabledSourceRoots();
        }
        if (disabledRoots.remove(renamed)) {
            disabledRoots.add(newRoot);
            storeDisabledSourceRoots(disabledRoots);
        }
        storeAdditionalSourceRoots();
        storeSourceRootsOrder(baseDir, getSourceRoots(unorderedOriginalSourcePath), sourcePathPermutation);
        return true;
    }
     */
    
    /**
     * Adds property change listener.
     *
     * @param l new listener.
     */
    @Override
    public void addPropertyChangeListener (PropertyChangeListener l) {
        pcs.addPropertyChangeListener (l);
    }

    /**
     * Removes property change listener.
     *
     * @param l removed listener.
     */
    @Override
    public void removePropertyChangeListener (
        PropertyChangeListener l
    ) {
        pcs.removePropertyChangeListener (l);
    }
    
    
    // helper methods ..........................................................
    
    /**
     * Normalizes the given path by removing unnecessary "." and ".." sequences.
     * This normalization is needed because the compiler stores source paths like "foo/../inc.jsp" into .class files. 
     * Such paths are not supported by our ClassPath API.
     * TODO: compiler bug? report to JDK?
     * 
     * @param path path to normalize
     * @return normalized path without "." and ".." elements
     */ 
    public static String normalize(String path) {
      for (Matcher m = thisDirectoryPattern.matcher(path); m.find(); )
      {
        path = m.replaceAll("$1");
        m = thisDirectoryPattern.matcher(path);
      }
      for (Matcher m = parentDirectoryPattern.matcher(path); m.find(); )
      {
        if (!m.group(2).equals("..")) {
          path = path.substring(0, m.start()) + m.group(1) + path.substring(m.end());
          m = parentDirectoryPattern.matcher(path);        
        }
      }
      return path;
    }
    
    /**
     * Returns source root for given ClassPath root as String, or <code>null</code>.
     */
    public static String getRoot(FileObject fileObject) {
        File f = null;
        String path = "";
        try {
            if (fileObject.getFileSystem () instanceof JarFileSystem) {
                f = ((JarFileSystem) fileObject.getFileSystem ()).getJarFile ();
                if (!fileObject.isRoot()) {
                    path = "!/"+fileObject.getPath();
                }
            } else {
                f = FileUtil.toFile (fileObject);
            }
        } catch (FileStateInvalidException ex) {
        }
        if (f != null) {
            return f.getAbsolutePath () + path;
        } else {
            return null;
        }
    }

    /**
     * Returns FileObject for given String.
     */
    private static FileObject getFileObject (String file) {
        File f = new File (file);
        FileObject fo = FileUtil.toFileObject (FileUtil.normalizeFile(f));
        String path = null;
        if (fo == null && file.contains("!/")) {
            int index = file.indexOf("!/");
            f = new File(file.substring(0, index));
            fo = FileUtil.toFileObject (f);
            path = file.substring(index + "!/".length());
        }
        if (fo != null && FileUtil.isArchiveFile (fo)) {
            fo = FileUtil.getArchiveRoot (fo);
            if (path !=null) {
                fo = fo.getFileObject(path);
            }
        }
        return fo;
    }

    public static int[] createPermutation(String[] roots, Map<String, Integer> orderIndexes, String[] sortedRoots) {
        int n = roots.length;
        if (orderIndexes == null) {
            int[] perm = new int[n];
            for (int i = 0; i < n; i++) {
                sortedRoots[i] = roots[i];
                perm[i] = i;
            }
            return perm;
        }

        class IndexedRoot {
            String root;
            Integer index;
            int order;
            IndexedRoot(String root, Integer index, int order) {
                this.root = root;
                this.index = index;
                this.order = order;
            }
        }
        IndexedRoot[] indexedRoots = new IndexedRoot[n];
        List<IndexedRoot> indexed = new ArrayList<IndexedRoot>();
        for (int i = 0; i < n; i++) {
            Integer index = orderIndexes.get(roots[i]);
            indexedRoots[i] = new IndexedRoot(roots[i], index, i);
            if (index != null) {
                indexed.add(indexedRoots[i]);
            }
        }
        class Cmp implements Comparator<IndexedRoot> {
            @Override
            public int compare(IndexedRoot ir1, IndexedRoot ir2) {
                Integer i1 = ir1.index;
                Integer i2 = ir2.index;
                return i1 - i2;
            }
        }
        Cmp cmp = new Cmp();
        if (indexed.size() == indexedRoots.length) {
            // All elements have index != null
            Arrays.sort(indexedRoots, cmp);
        } else if (!indexed.isEmpty()) {
            // Sort only the elements with index != null
            Collections.sort(indexed, cmp);
            // and merge them in in the correct order:
            int indexedi = 0;
            for (int i = 0; i < n; i++) {
                if (indexedRoots[i].index != null) {
                    indexedRoots[i] = indexed.get(indexedi++);
                }
            }
        }

        int[] perm = new int[n];
        for (int i = 0; i < n; i++) {
            sortedRoots[i] = indexedRoots[i].root;
            perm[i] = indexedRoots[i].order;
        }
        return perm;
    }
    
    private ClassPath reorder(ClassPath sourcePath, final Map<String, Integer> orderIndexes) {
        String[] roots = getSourceRoots(sourcePath);
        class Cmp implements Comparator<String> {
            @Override
            public int compare(String o1, String o2) {
                int i1 = orderIndexes.get(o1);
                int i2 = orderIndexes.get(o2);
                return i1 - i2;
            }
        }
        Cmp cmp = new Cmp();
        Arrays.sort(roots, cmp);
        return createClassPath(roots);
    }

    private static ClassPath createClassPath(String[] roots) {
        int n = roots.length;
        FileObject[] froots = new FileObject[n];
        for (int i = 0; i < n; i++) {
            froots[i] = getFileObject(roots[i]);
        }
        return createClassPath(froots);
    }

    private static ClassPath createClassPath(FileObject[] froots) {
        List<PathResourceImplementation> pris = new ArrayList<PathResourceImplementation> ();
        for (FileObject fo : froots) {
            if (fo != null && fo.canRead()) {
                try {
                    URL url = fo.toURL();
                    pris.add(ClassPathSupport.createResource(url));
                } catch (IllegalArgumentException iaex) {
                    // Can be thrown from ClassPathSupport.createResource()
                    // Ignore - bad source root
                    //logger.log(Level.INFO, "Invalid source root = "+fo, iaex);
                    logger.warning(iaex.getLocalizedMessage());
                }
            }
        }
        return ClassPathSupport.createClassPath(pris);
    }
    
    private static ClassPath createClassPath(URL[] urls) {
        List<PathResourceImplementation> pris = new ArrayList<PathResourceImplementation> ();
        for (URL url : urls) {
            FileObject fo = URLMapper.findFileObject(url);
            if (fo != null && fo.canRead()) {
                try {
                    pris.add(ClassPathSupport.createResource(url));
                } catch (IllegalArgumentException iaex) {
                    // Can be thrown from ClassPathSupport.createResource()
                    // Ignore - bad source root
                    //logger.log(Level.INFO, "Invalid source root = "+fo, iaex);
                    logger.warning(iaex.getLocalizedMessage());
                }
            }
        }
        return ClassPathSupport.createClassPath(pris);
    }
    
    private ArtifactsUpdatedImpl addArtifactsUpdateListenerFor(JPDADebugger debugger, FileObject src) {
        URL url = src.toURL();
        ArtifactsUpdatedImpl l = new ArtifactsUpdatedImpl(debugger, url, src);
        BuildArtifactMapper.addArtifactsUpdatedListener(url, l);
        return l;
    }

    private static List<URL> getURLRoots(ClassPath cp) {
        List<URL> urls = new ArrayList<URL>();
        for (Entry entry : cp.entries()) {
            URL url = entry.getURL();
            urls.add(url);
        }
        return urls;
    }

    private static boolean CAN_FIX_CLASSES_AUTOMATICALLY = Boolean.getBoolean("debugger.apply-code-changes.on-save"); // NOI18N

    private static class ArtifactsUpdatedImpl implements ArtifactsUpdated {

        private Reference<JPDADebugger> debuggerRef;
        private final URL url;
        private FileObject src;

        public ArtifactsUpdatedImpl(JPDADebugger debugger, URL url, FileObject src) {
            this.debuggerRef = new WeakReference<JPDADebugger>(debugger);
            this.url = url;
            this.src = src;
        }

        public URL getURL() {
            return url;
        }

        @Override
        public void artifactsUpdated(Iterable<File> artifacts) {
            String error = null;
            final JPDADebugger debugger = debuggerRef.get();
            if (debugger == null) {
                error = NbBundle.getMessage(SourcePathProviderImpl.class, "MSG_NoJPDADebugger");
            } else if (!debugger.canFixClasses()) {
                error = NbBundle.getMessage(SourcePathProviderImpl.class, "MSG_CanNotFix");
            } else if (debugger.getState() == JPDADebugger.STATE_DISCONNECTED) {
                error = NbBundle.getMessage(SourcePathProviderImpl.class, "MSG_NoDebug");
            }

            boolean canFixClasses = Properties.getDefault().getProperties("debugger.options.JPDA").
                    getBoolean("ApplyCodeChangesOnSave", CAN_FIX_CLASSES_AUTOMATICALLY);
            if (logger.isLoggable(Level.FINE)) {
                logger.fine("artifactsUpdated("+artifacts+") error = '"+error+"', canFixClasses = "+canFixClasses);
            }
            if (error == null) {
                if (!canFixClasses) {
                    for (File f : artifacts) {
                        FileObject fo = FileUtil.toFileObject(f);
                        if (fo != null) {
                            String className = fileToClassName(fo);
                            if (className != null) {
                                FixClassesSupport.ClassesToReload.getInstance().addClassToReload(
                                        debugger, src, className, fo);
                            }
                        }
                    }
                    return ;
                }
                Map<String, FileObject> classes = new HashMap<String, FileObject>();
                for (File f : artifacts) {
                    FileObject fo = FileUtil.toFileObject(f);
                    if (fo != null) {
                        String className = fileToClassName(fo);
                        if (className != null) {
                            classes.put(className, fo);
                        }
                    }
                }
                FixClassesSupport.reloadClasses(debugger, classes);
            } else {
                BuildArtifactMapper.removeArtifactsUpdatedListener(url, this);
            }

            if (error != null && canFixClasses) {
                FixClassesSupport.notifyError(debugger, error);
            }
        }

        private static String fileToClassName (FileObject fo) {
            // remove ".class" from and use dots for for separator
            ClassPath cp = ClassPath.getClassPath (fo, ClassPath.EXECUTE);
            if (cp == null) {
                logger.log(Level.WARNING, "Did not find EXECUTE class path for {0}", fo);
                return null;
            }
    //        FileObject root = cp.findOwnerRoot (fo);
            return cp.getResourceName (fo, '.', false);
        }
    }

    private class PathRegistryListener implements GlobalPathRegistryListener, PropertyChangeListener {

        private RequestProcessor rp = new RequestProcessor(PathRegistryListener.class.getName(), 1);
        private RequestProcessor.Task task;
        private List<URL> addedRoots = null;
        private List<URL> removedRoots = null;
        private final Object rootsLock = new Object();

        public PathRegistryListener() {
            task = rp.create(new Runnable() {
                @Override
                public void run() {
                    rootsChanged();
                }
            });
        }
        
        @Override
        public void pathsAdded(final GlobalPathRegistryEvent event) {
            List<URL> changedPaths = getChangedPaths(event);
            if (changedPaths == null) {
                return ;
            }
            synchronized (rootsLock) {
                if (addedRoots == null) {
                    addedRoots = changedPaths;
                } else {
                    addedRoots.addAll(changedPaths);
                }
            }
            task.schedule(1000);    // Work with class path is expensive.
        }
        
        @Override
        public void pathsRemoved(final GlobalPathRegistryEvent event) {
            List<URL> changedPaths = getChangedPaths(event);
            if (changedPaths == null) {
                return ;
            }
            synchronized (rootsLock) {
                if (removedRoots == null) {
                    removedRoots = changedPaths;
                } else {
                    removedRoots.addAll(changedPaths);
                }
            }
            task.schedule(1000);    // Work with class path is expensive.
        }
        
        private List<URL> getChangedPaths(final GlobalPathRegistryEvent event) {
            if (!ClassPath.SOURCE.equals(event.getId())) {
                return null;
            }
            List<URL> urls = new ArrayList<URL>();
            for (ClassPath cp : event.getChangedPaths()) {
                for (Entry entry : cp.entries()) {
                    URL url = entry.getURL();
                    urls.add(url);
                }
            }
            return urls;
        }
        
        private void rootsChanged() {
            List<URL> added;
            List<URL> removed;
            synchronized (rootsLock) {
                added = addedRoots;
                removed = removedRoots;
                addedRoots = null;
                removedRoots = null;
            }
            boolean changed = false;
            if (added != null && added.size() > 0) {
                synchronized (SourcePathProviderImpl.this) {
                    if (originalSourcePath == null) {
                        return ;
                    }
                    List<URL> sourcePaths = getURLRoots(originalSourcePath);
                    sourcePaths.addAll(added);
                    originalSourcePath =
                            SourcePathProviderImpl.createClassPath(
                                sourcePaths.toArray(new URL[0]));

                    sourcePaths = getURLRoots(smartSteppingSourcePath);
                    sourcePaths.addAll(added);
                    smartSteppingSourcePath =
                            SourcePathProviderImpl.createClassPath(
                                sourcePaths.toArray(new URL[0]));
                }
                changed = true;
            }
            if (removed != null && removed.size() > 0) {
                synchronized (SourcePathProviderImpl.this) {
                    if (originalSourcePath == null) {
                        return ;
                    }
                    List<URL> sourcePaths = getURLRoots(originalSourcePath);
                    sourcePaths.removeAll(removed);
                    originalSourcePath =
                            SourcePathProviderImpl.createClassPath(
                                sourcePaths.toArray(new URL[0]));

                    sourcePaths = getURLRoots(smartSteppingSourcePath);
                    sourcePaths.removeAll(removed);
                    smartSteppingSourcePath =
                            SourcePathProviderImpl.createClassPath(
                                sourcePaths.toArray(new URL[0]));
                }
                changed = true;
            }
            if (changed) {
                // Clear caches so that the new source roots are taken into account
                synchronized (urlCache) {
                    urlCache.clear();
                }
                synchronized (urlCacheGlobal) {
                    urlCacheGlobal.clear();
                }
                pcs.firePropertyChange (PROP_SOURCE_ROOTS, null, null);
            }
        }

        @Override
        public void propertyChange(final PropertyChangeEvent evt) {
            // JDK sources changed
            // Work with class path is expensive. Move it off AWT.
            if (EventQueue.isDispatchThread()) {
                rp.post(new Runnable() {
                    @Override
                    public void run() {
                        propertyChange(evt);
                    }
                });
                return ;
            }
            JavaPlatform[] platforms = JavaPlatformManager.getDefault ().
                getInstalledPlatforms ();
            boolean changed = false;
            synchronized (SourcePathProviderImpl.this) {
                if (originalSourcePath == null) {
                    return ;
                }
                platformSourceRoots.clear();
                List<FileObject> sourcePaths = new ArrayList<FileObject>(
                        Arrays.asList(originalSourcePath.getRoots()));
                for(JavaPlatform jp : platforms) {
                    FileObject[] roots = jp.getSourceFolders().getRoots ();
                    for (FileObject fo : roots) {
                        if (!sourcePaths.contains(fo)) {
                            sourcePaths.add(fo);
                            changed = true;
                        }
                    }
                    platformSourceRoots.addAll(getSourceRootsSet(jp.getSourceFolders()));
                }
                if (changed) {
                    originalSourcePath =
                            SourcePathProviderImpl.createClassPath(
                                sourcePaths.toArray(new FileObject[0]));
                }
            }
            if (changed) {
                // Clear caches so that the new source roots are taken into account
                synchronized (urlCache) {
                    urlCache.clear();
                }
                synchronized (urlCacheGlobal) {
                    urlCacheGlobal.clear();
                }
                pcs.firePropertyChange (PROP_SOURCE_ROOTS, null, null);
            }
        }
    }
    
    public static final class FileObjectComparator implements Comparator<FileObject> {

        @Override
        public int compare(FileObject fo1, FileObject fo2) {
            String r1 = getRoot(fo1);
            String r2 = getRoot(fo2);
            if (r1 == null) {
                return -1;
            }
            if (r2 == null) {
                return +1;
            }
            return r1.compareTo(r2);
        }
        
    }

    private static final class URLCacheMap extends LinkedHashMap<String, String> {

        private static final int URL_CACHE_SIZE = 500;

        public URLCacheMap() {
            super(URL_CACHE_SIZE, .1f, true);
        }

        @Override
        protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
            return size() >= URL_CACHE_SIZE;
        }

    }
}
