/*
 * 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;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.lang.reflect.Method;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.nio.file.NoSuchFileException;
import java.security.CodeSource;
import java.security.PermissionCollection;
import java.security.Policy;
import java.security.ProtectionDomain;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import org.openide.util.BaseUtilities;
import org.openide.util.Exceptions;

/**
 * A ProxyClassLoader capable of loading classes from a set of jar files
 * and local directories.
 *
 * @author  Petr Nejedly
 */
public class JarClassLoader extends ProxyClassLoader {
    private static Stamps cache;
    static Archive archive = new Archive(); 

    static void initializeCache() {
        cache = Stamps.getModulesJARs();
        archive = new Archive(cache);
        PackageAttrsCache.initialize();
    }
    
    /**
     * Creates a new archive or updates existing archive with the necessary
     * resources gathered so far. It also stops gatheing and serving
     * additional request, if it was still doing so.
     */    
    public static void saveArchive() {
        if (cache != null) {
            try {
                archive.save(cache);
            } catch (IOException ioe) {
                LOGGER.log(Level.WARNING, "saving archive", ioe);
            }
        } else {
            archive.stopGathering();
            archive.stopServing();
        }
    }
    
    /** Check whether the archive has already been populated during 
     * previous executions.
     * 
     * @return true, if the archive is ready and non-empty
     * @since 2.61
     */
    public static boolean isArchivePopulated() {
        return archive != null && archive.isPopulated();
    }
    
    static {
        ProxyURLStreamHandlerFactory.register();
    }
    
    private static final Logger LOGGER = Logger.getLogger(JarClassLoader.class.getName());

    private Source[] sources;
    private Module module;
    
    /** Creates new JarClassLoader.
     * Gives transitive flag as true.
     */
    public JarClassLoader(List<File> files, ClassLoader[] parents) {
        this(files, parents, true, null);
    }
    
    public JarClassLoader(List<File> files, ClassLoader[] parents, boolean transitive) {
        this(files, parents, transitive, null);
    }
    /** Creates new JarClassLoader.
     * @since org.netbeans.core/1 > 1.6
     * @see ProxyClassLoader#ProxyClassLoader(ClassLoader[],boolean)
     */
    public JarClassLoader(List<File> files, ClassLoader[] parents, boolean transitive, Module mod) {
        super(parents, transitive);
        this.module = mod;
        List<Source> l = new ArrayList<Source>(files.size());
        try {
            for (File file : files) {
                l.add(Source.create(file, this));
            }
        } catch (IOException exc) {
            throw new IllegalArgumentException(exc.getMessage());
        }
        sources = l.toArray(new Source[l.size()]);
        // overlaps with old packages doesn't matter,PCL uses sets.
        addCoveredPackages(getCoveredPackages(module, sources));
    }

    final void addURL(URL location) throws IOException, URISyntaxException {
        File f = BaseUtilities.toFile(location.toURI());
        assert f.exists() : "URL must be existing local file: " + location;

        List<Source> arr = new ArrayList<Source>(Arrays.asList(sources));
        arr.add(new JarSource(f));

        synchronized (sources) {
            sources = arr.toArray(new Source[arr.size()]);
        }

        // overlaps with old packages doesn't matter,PCL uses sets.
        addCoveredPackages(getCoveredPackages(module, sources));
    }

    /** Allows to specify the right permissions, OneModuleClassLoader does it differently.
     */
    protected PermissionCollection getPermissions( CodeSource cs ) {           
        return Policy.getPolicy().getPermissions(cs);       
    }        
    
    
    protected Package definePackage(String name, Manifest man, URL url)
    throws IllegalArgumentException {
        if (man == null) {
            return definePackage(name, null, null, null, null, null, null, null);
        }

        String path = name.replace('.', '/').concat("/"); // NOI18N
        String[] arr = PackageAttrsCache.findPackageAttrs(url, man, path);
        URL sealBase = "true".equalsIgnoreCase(arr[6]) ? url : null; // NOI18N
        return definePackage(name, arr[0], arr[1], arr[2],
            arr[3], arr[4], arr[5], sealBase);
    }
    
    /**
     * Bytecode patching helper
     */
    private PatchByteCode patchingBytecode;
    
    byte[] getClassData(String name) {
        String path = name.replace('.', '/').concat(".class"); // NOI18N
        for( int i=0; i<sources.length; i++ ) {
            final Source src = sources[i];
            byte[] data = src.getClassData(path);
            if (data != null) {
                return data;
            }
        }
        return null;
    }
    
    @Override
    protected Class<?> doLoadClass(String pkgName, String name) {
        String path = name.replace('.', '/').concat(".class"); // NOI18N
        
        // look up the Sources and return a class based on their content
        for( int i=0; i<sources.length; i++ ) {
            final Source src = sources[i];
            byte[] data = src.getClassData(path);
            if (data == null) continue;

            synchronized (sources) {
                if (patchingBytecode == null) {
                    Enumeration<URL> res = findResources("META-INF/.bytecodePatched"); // NOI18N
                    if (res.hasMoreElements()) {
                        LOGGER.log(Level.FINE, "Patching bytecode in {0}", this);
                    }
                    patchingBytecode = PatchByteCode.fromStream(res, this);
                }
            }
            try {
                data = patchingBytecode.apply(name, data);
            } catch (Exception x) {
                LOGGER.log(Level.INFO, "Could not bytecode-patch " + name, x);
            }
            
            // Note that we assume that if we are defining a class in this package,
            // we should also define the package! Thus recurse==false.
            // However special packages might be defined in a parent and then we want
            // to have the same Package object, proper sealing check, etc.; so be safe,
            // overhead is probably small (check in parents, nope, check super which
            // delegates to system loaders).
            Package pkg = getPackageFast(pkgName, true);
            if (pkg != null) {
                // XXX full sealing check, URLClassLoader does something more
                if (pkg.isSealed() && !pkg.isSealed(src.getURL())) throw new SecurityException("sealing violation"); // NOI18N
            } else {
                class DelayedManifest extends Manifest {
                    private Manifest delegate;
                    
                    private Manifest delegate() {
                        if (delegate == null) {
                            Manifest m;
                            m = module == null || src != sources[0] ? src.getManifest() : module.getManifest();
                            if (m == null) {
                                m = new Manifest();
                            }
                            delegate = m;
                            return m;
                        }
                        return delegate;
                    }
                    
                    @Override
                    public Attributes getMainAttributes() {
                        return delegate().getMainAttributes();
                    }

                    @Override
                    public Attributes getAttributes(String name) {
                        return delegate().getAttributes(name);
                    }

                    @Override
                    public Map<String, Attributes> getEntries() {
                        return delegate().getEntries();
                    }
                }
                Manifest man = new DelayedManifest();
                try {
                    definePackage(pkgName, man, src.getURL());
                } catch (IllegalArgumentException x) {
                    // #156478: possibly a race condition defining packages in parallel parents? Ignore.
                    LOGGER.log(Level.FINE, null, x);
                }
            }
            try {
                data = NbInstrumentation.patchByteCode(this, name, src.getProtectionDomain(), data);
            } catch (IllegalClassFormatException ex) {
                LOGGER.log(Level.WARNING, "Problems patching" + name, ex);
            }
            return defineClass (name, data, 0, data.length, src.getProtectionDomain());
        } 
        return null;
    }
    // look up the jars and return a resource based on a content of jars
    @Override
    public URL findResource(String name) {
        for( int i=0; i<sources.length; i++ ) {
            URL item = sources[i].getResource(name);
            if (item != null) return item;
        }
	return null;
    }

    @Override
    public Enumeration<URL> findResources(String name) {
        Vector<URL> v = new Vector<URL>(3);
        // look up the jars and return a resource based on a content of jars

        for( int i=0; i<sources.length; i++ ) {
            URL item = sources[i].getResource(name);
            if (item != null) v.add(item);
        }
        return v.elements();
    }
    
    public @Override void destroy() {
        super.destroy ();
        for (Source src : sources) {
            try {
                src.destroy();
            } catch (IOException ioe) {
                LOGGER.log(Level.WARNING, "could not destroy " + src, ioe);
            }
        }
    }

    /** package-private method useful only for testing.
     * Used from JarClassLoaderTest to force close before reopening. */
    void releaseJars() throws IOException {
        for (Source src : sources) {
            if (src instanceof JarSource) {
                ((JarSource)src).doCloseJar();
            }
        }
    }

    static abstract class Source {
        private URL url;
        private ProtectionDomain pd;
        protected JarClassLoader jcl;
        private static Map<String,Source> sources = new HashMap<String, Source>();
        
        public Source(URL url) {
            this.url = url;
        }
        
        public final URL getURL() {
            return url;
        }

        public abstract String getPath();
        
        public final ProtectionDomain getProtectionDomain() {
            if (pd == null) {
                CodeSource cs = new CodeSource(url, (Certificate[])null);
                pd = new ProtectionDomain(cs, jcl.getPermissions(cs));
            }
            return pd;
        }
  
        public final URL getResource(String name) {
            try {
                return doGetResource(name);
            } catch (Exception e) {
                // can't get the resource. E.g. already closed JarFile
                LOGGER.log(Level.FINE, null, e);
            }
            return null;
        }
        
        protected abstract URL doGetResource(String name) throws IOException;
        
        public final byte[] getClassData(String path) {
            try {
                return readClass(path);
            } catch (IOException e) {
                LOGGER.log(Level.WARNING, "looking up " + path, e);
            }
            return null;
        }

        protected abstract byte[] readClass(String path) throws IOException;

        public Manifest getManifest() {
            return null;
        }

        protected abstract void listCoveredPackages(Set<String> known, StringBuffer save);
        
        protected void destroy() throws IOException {
            // relatively slow (millis instead of micros),
            // but rare enough to not matter
            sources.values().remove(this);
        }
        
        static Source create(File f, JarClassLoader jcl) throws IOException {
            boolean directory;
            if (f.getName().endsWith("jar")) {
                directory = false;
            } else {
                directory = f.isDirectory();
            }
            Source src = directory ? new DirSource(f) : new JarSource(f);
            src.jcl = jcl;
            // should better use the same string as other indexes
            // this way, there are currently 3 similar long Strings per
            // JarClassLoader instance - its URL, its identifier
            // in Archive.sources map and this one
            sources.put(src.getPath(), src);
            return src;
        }

        @Override
        public String toString() {
            return url.toString();
        }

    }
    
    static void dumpFiles(File f, int retry) {
        for (;;) {
            if (f == null) {
                LOGGER.log(Level.INFO, "file {0} is null. # of retries {1}", new Object[]{f, retry}); // NOI18N
                break;
            }
            if (f.exists()) {
                LOGGER.log(Level.INFO, "file {0} exists. # of retries {1}", new Object[]{f, retry}); // NOI18N
                if (f.isDirectory()) {
                    LOGGER.log(Level.INFO, "{0} is directory and contains: {1}", new Object[]{f, Arrays.toString(f.list())}); // NOI18N
                } else {
                    LOGGER.log(Level.INFO, "{0} isDirectory: {1}, isFile: {2} size: {3}", new Object[]{f, f.isDirectory(), f.isFile(), f.length()}); // NOI18N
                }
                break;
            }
            LOGGER.log(Level.INFO, "{0} does not exist, # of retries {1}", new Object[]{f, retry}); // NOI18N
            f = f.getParentFile();
        }
    }

    static class JarSource extends Source implements ArchiveResources {
        private String resPrefix;
        private File file;

        private Future<JarFile> fjar;
        private boolean dead;
        private int requests;
        private int used;
        private volatile Reference<Manifest> manifest;
        /** #141110: expensive to repeatedly look for them */
        private final Set<String> nonexistentResources = Collections.synchronizedSet(new HashSet<String>());
        private final Set<File> warnedFiles = Collections.synchronizedSet(new HashSet<File>()); // #183696
        
        JarSource(File file) throws IOException {
            this(file, toURI(file));
        }
        private JarSource(File file, String resPrefix) throws IOException {
            super(new URL(resPrefix)); // NOI18N
            this.resPrefix = resPrefix; // NOI18N;
            this.file = file;
        }

        @Override
        public String getPath() {
            return file.getPath();
        }

        private static String toURI(final File file) {
            class VFile extends File {
                public VFile() {
                    super(file.getPath());
                }

                @Override
                public boolean isDirectory() {
                    return false;
                }

                @Override
                public File getAbsoluteFile() {
                    return this;
                }
            }
            return "jar:" + BaseUtilities.toURI(new VFile()) + "!/"; // NOI18N
        }

        @Override
        public Manifest getManifest() {
            {
                Manifest man;
                if (manifest != null && (man = manifest.get()) != null) {
                    return man;
                }
            }
            try {
                byte[] arr = archive.getData(this, "META-INF/MANIFEST.MF");
                if (arr == null) {
                    return null;
                }
                final Manifest man = new Manifest(new ByteArrayInputStream(arr));
                manifest = new SoftReference<Manifest>(man);
                return man;
            } catch (IOException ex) {
                LOGGER.log(Level.WARNING, "Cannot read manifest for " + getPath(), ex);
                return null;
            }
        }
        
        JarFile getJarFile(final String forWhat) throws IOException {
            FutureTask<JarFile> init = null;
            synchronized(sources) {
                requests++;
                used++;
                if (fjar == null) {
                    fjar = sources.get(this);
                    if (fjar == null) {
                        fjar = init = new FutureTask<JarFile>(new Callable<JarFile>() {
                            @Override
                            public JarFile call() throws IOException {
                                int retry = 0;
                                for (;;) {
                                    try {
                                        long now = System.currentTimeMillis();
                                        JarFile ret;
                                        try {
                                            ret = new JarFile(file, false);
                                        } catch (FileNotFoundException | NoSuchFileException ex) {
                                            throw (ZipException)new ZipException(ex.getMessage()).initCause(ex);
                                        }
                                        long took = System.currentTimeMillis() - now;
                                        opened(JarClassLoader.JarSource.this, forWhat);
                                        if (took > 500) {
                                            LOGGER.log(Level.WARNING, "Opening {0} took {1} ms", new Object[]{file, took}); // NOI18N
                                        }
                                        return ret;
                                    } catch (ZipException zip) {
                                        if (file.exists() && retry++ < 3) {
                                            LOGGER.log(Level.WARNING, "Error opening " + file + " (exists=" + file.exists() + ") retry: " + retry, zip); // NOI18N
                                            opened(JarClassLoader.JarSource.this, "ziperror");
                                            continue;
                                        }
                                        dumpFiles(file, retry);
                                        throw zip;
                                    }
                                }
                            }
                        });
                        sources.put(this, fjar);
                    }
                }
            }
            if (init != null) init.run();
            return callGet();
        }
        
        private void releaseJarFile() {
            synchronized(sources) {
                assert used > 0;
                used--;
            }
        }
        
        
        @Override
        protected URL doGetResource(String name) throws IOException  {
            byte[] buf = archive.getData(this, name);
            if (buf == null) return null;
            if (LOGGER.isLoggable(Level.FINER)) {
                LOGGER.log(Level.FINER, "Loading {0} from {1}", new Object[] {name, file.getPath()});
            }
            try {
                return new URL(null, resPrefix + new URI(null, name, null).getRawPath(), new JarURLStreamHandler(jcl));
            } catch (URISyntaxException x) {
                throw new IOException(name + " in " + resPrefix + ": " + x.toString(), x);
            }
        }
        
        @Override
        protected byte[] readClass(String path) throws IOException {
            try {
                return archive.getData(this, path);
            } catch (ZipException ex) {
                dumpFiles(file, -1);
                throw ex;
            }
        }
        
        @Override
        public byte[] resource(String path) throws IOException {
            if (nonexistentResources.contains(path)) {
                return null;
            }
            JarFile jf;
            try {
                jf = getJarFile(path);
            } catch (ZipException ex) {
                if (warnedFiles.add(file)) {
                    LOGGER.log(Level.INFO, "Cannot open " + file, ex);
                    dumpFiles(file, -1);
                }
                return null;
            }
            try {
                ZipEntry ze = jf.getEntry(path);
                if (ze == null) {
                    nonexistentResources.add(path);
                    return null;
                }

                if (LOGGER.isLoggable(Level.FINER)) {
                    LOGGER.log(Level.FINER, "Loading {0} from {1}", new Object[] {path, file.getPath()});
                }
            
                int len = (int)ze.getSize();
                byte[] data = new byte[len];
                InputStream is = jf.getInputStream(ze);
                int count = 0;
                while (count < len) {
                    count += is.read(data, count, len-count);
                }
                return data;
            } finally {
                releaseJarFile();
            }
        }


        @Override
        protected void listCoveredPackages(Set<String> known, StringBuffer save) {
            try {
                JarFile src = getJarFile("pkg");

                Enumeration<JarEntry> en = src.entries();
                while (en.hasMoreElements()) {
                    JarEntry je = en.nextElement();
                    if (! je.isDirectory()) {
                        String itm = je.getName();
                        int slash = itm.lastIndexOf('/');
                        if (slash == -1) {
                            // resource in default package
                            String res = "default/" + je.getName();
                            if (known.add(res)) {
                                save.append(res).append(',');
                            }
                            continue;
                        }
                        if (itm.startsWith("META-INF/")) {
                            String res = itm.substring(8); // "/services/pkg.Service"
                            if (known.add(res)) save.append(res).append(',');
                            continue;
                        }
                        String pkg = slash > 0 ? itm.substring(0, slash).replace('/','.') : "";
                        if (known.add(pkg)) save.append(pkg).append(',');
                    }
                }
            } catch (ZipException x) { // Unix
                if (warnedFiles.add(file)) {
                    LOGGER.log(Level.INFO, "Cannot open " + file, x);
                    dumpFiles(file, -1);
                }
            } catch (FileNotFoundException x) { // Windows
                if (warnedFiles.add(file)) {
                    LOGGER.log(Level.INFO, "Cannot open " + file, x);
                    dumpFiles(file, -1);
                }
            } catch (IOException ioe) {
                if (warnedFiles.add(file)) {
                    LOGGER.log(Level.WARNING, "problems with " + file, ioe);
                    dumpFiles(file, -1);
                }
            } finally {
                releaseJarFile();
            }
        }

        
        @Override
        protected void destroy() throws IOException {
            super.destroy();
            if (dead) {
                return;
            }
            
            File orig = file;

            if (!orig.isFile()) {
                // Can happen when a test module is deleted:
                // the physical JAR has already been deleted
                // when the module was disabled. In this case it
                // is possible that a classloader request for something
                // in the JAR could still come in. Does it matter?
                // See comment in Module.cleanup.
                return;
            }
            
            String name = orig.getName();
            String prefix, suffix;
            int idx = name.lastIndexOf('.');
            if (idx == -1) {
                prefix = name;
                suffix = null;
            } else {
                prefix = name.substring(0, idx);
                suffix = name.substring(idx);
            }
            
            while (prefix.length() < 3) prefix += "x"; // NOI18N
            File temp = File.createTempFile(prefix, suffix);
            temp.deleteOnExit();

            InputStream is = new FileInputStream(orig);
            try {
                OutputStream os = new FileOutputStream(temp);
                try {
                    byte[] buf = new byte[4096];
                    int j;
                    while ((j = is.read(buf)) != -1) {
                        os.write(buf, 0, j);
                    }
                } finally {
                    os.close();
                }
            } finally {
                is.close();
            }
 
            doCloseJar();
            file = temp;
            dead = true;
            if (LOGGER.isLoggable(Level.FINE)) {
                LOGGER.log(Level.FINE, "#21114: replacing {0} with {1}", new Object[] {orig, temp});
            }
        }
        
        private JarFile callGet() throws IOException {
            boolean interrupted = false;
            JarFile ret;
            for (;;) {
                try {
                    ret = fjar.get();
                    break;
                } catch (InterruptedException ex) {
                    interrupted = true;
                } catch (ExecutionException ex) {
                    Throwable cause = ex.getCause();
                    if (cause instanceof IOException) {
                        // This is important for telling general IOException from ZipException
                        // down the stack.
                        throw (IOException)cause;
                    } else if (cause instanceof ThreadDeath) {
                        throw (ThreadDeath) cause; // #201098
                    } else {
                        throw new IOException(cause);
                    }
                }
            }
            if (interrupted) {
                Thread.currentThread().interrupt();
            }
            return ret;
        }

        private void doCloseJar() throws IOException {
            JarFile jar = null;
            synchronized(sources) {
                if (fjar != null) {
                    jar = callGet();
                    if (sources.remove(this) == null) {
                        LOGGER.warning("Can't remove " + this);
                    }
                    LOGGER.log(Level.FINE, "Closing JAR {0}", jar.getName());
                    fjar = null;
                    LOGGER.log(Level.FINE, "Remaining open JARs: {0}", sources.size());
                }
            }
            if (jar != null) jar.close();
        }

        /** Delete any temporary JARs we were holding on to.
         * Also close any other JARs in our list.
         */
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            
            doCloseJar();

            if (dead) {
                LOGGER.log(Level.FINE, "#21114: closing and deleting temporary JAR {0}", file);
                if (file.isFile() && !file.delete()) {
                    LOGGER.log(Level.FINE, "(but failed to delete {0})", file);
                }
            }
        }

        // JarFile pool tracking
        private static final Map<JarSource, Future<JarFile>> sources = new HashMap<JarSource, Future<JarFile>>();
        private static int LIMIT = Integer.getInteger("org.netbeans.JarClassLoader.limit_fd", 300);

        static void opened(JarSource source, String forWhat) {
            synchronized (sources) {
                if (sources.size() > LIMIT) {
                    // close something
                    JarSource toClose = toClose(source);
                    try {
                        toClose.doCloseJar();
                    } catch (IOException ioe) {
                        LOGGER.log(Level.INFO, "closing " + toClose, ioe);
                    }
                }
                if (LOGGER.isLoggable(Level.FINE)) {
                    LOGGER.log(Level.FINE, "Opening module JAR {0} for {1}", new Object[] {source.file, forWhat});
                    LOGGER.log(Level.FINE, "Currently open JARs: {0}", sources.size());
                }
            }
        }

        // called under lock(sources) 
        private static JarSource toClose(JarSource notThisOne) {
            assert Thread.holdsLock(sources);
             
            int min = Integer.MAX_VALUE; 
            JarSource candidate = null; 
            for (JarSource act : sources.keySet()) {
                // aging: slight exponential decay of all opened sources?
                act.requests = 5*act.requests/6;
                
                if (act.used > 0) continue;
                if (act.requests < min) { 
                    min = act.requests; 
                    candidate = act; 
                } 
            } 
             
            assert candidate != null;
            assert candidate != notThisOne : "Closing just opened JarSource: " + notThisOne;
            return candidate; 
        }

        @Override
        public String getIdentifier() {
            String tmp = getURL().toExternalForm();
            if (tmp.startsWith("jar:file:") && tmp.endsWith("!/")) {
                String path = tmp.substring(9, tmp.length() - 2).replace("%20", " ");
                if (BaseUtilities.isWindows()) {
                    if (path.startsWith("/")) { // NOI18N
                        path = path.substring(1);
                    }
                    path = path.replace('/', File.separatorChar);
                }
                return Stamps.findRelativePath(path) + "!/";
            }
            return tmp;
        }
    }

    static class DirSource extends Source {
        File dir;
        Manifest manifest;
        
        DirSource(File file) throws MalformedURLException {
            super(BaseUtilities.toURI(file).toURL());
            dir = file;
        }
        
        public Manifest getManifest() {
            Manifest mf = manifest;
            if (mf != null) {
                return mf;
            }
            File maniF = new File(new File(dir, "META-INF"), "MANIFEST.MF");
            mf = new Manifest();
            if (maniF.canRead()) {
                try (InputStream istm = new FileInputStream(maniF)) {
                    mf.read(istm);
                } catch (IOException ex) {
                    Exceptions.printStackTrace(ex);
                }
            }
            return manifest = mf;
        }
        
        public String getPath() {
            return dir.getPath();
        }

        protected URL doGetResource(String name) throws MalformedURLException {
            File resFile = new File(dir, name);
            return resFile.exists() ? BaseUtilities.toURI(resFile).toURL() : null;
        }
        
        protected byte[] readClass(String path) throws IOException {
            File clsFile = new File(dir, path.replace('/', File.separatorChar));
            if (!clsFile.exists()) return null;
            
            int len = (int)clsFile.length();
            byte[] data = new byte[len];
            InputStream is = new FileInputStream(clsFile);
            try {
                int count = 0;
                while (count < len) {
                    count += is.read(data, count, len - count);
                }
                return data;
            } finally {
                is.close();
            }
        }
        
        protected void listCoveredPackages(Set<String> known, StringBuffer save) {
            appendAllChildren(known, save, dir, "");
        }
        
        private static void appendAllChildren(Set<String> known, StringBuffer save, File dir, String prefix) {
            boolean populated = false;
            for (File f : dir.listFiles()) {
                if (f.isDirectory()) {
                    appendAllChildren(known, save, f, prefix + f.getName() + '.');
                } else {
                    if (prefix.length() == 0) {
                        // resource in default package
                        String res = "default/" + f.getName();
                        if (known.add(res)) {
                            save.append(res).append(',');
                        }
                        continue;
                    }
                    populated = true;
                    if (prefix.startsWith("META-INF.")) {
                       String res = prefix.substring(8).replace('.', '/').concat(f.getName());
                       if (known.add(res)) save.append(res).append(',');
                    }
                }
            }
            if (populated) {
                String pkg = prefix;
                if (pkg.endsWith(".")) pkg = pkg.substring(0, pkg.length()-1);
                if (known.add(pkg)) save.append(pkg).append(',');
            }
        }
    }
    
    private static Iterable<String> getCoveredPackages(Module mod, Source[] sources) {
        if (mod != null) {
            Set<String> ret = mod.getCoveredPackages();
            if (ret != null) {
                return ret;
            }
        }
        
        Set<String> known = new HashSet<String>();
        Manifest m = mod == null ? null : mod.getManifest();
        if (m != null) {
            Attributes attr = m.getMainAttributes();
            String pack = attr.getValue("Covered-Packages"); // NOI18N
            if (pack != null) {
                known.addAll(Arrays.asList(pack.split(",", -1)));
                mod.registerCoveredPackages(known);
                return known;
            }
        }
        
        // not precomputed/cached, analyze
        StringBuffer save = new StringBuffer();
        for (Source s : sources) s.listCoveredPackages(known, save);

        if (save.length() > 0) save.setLength(save.length()-1);
        if (mod != null) {
            mod.registerCoveredPackages(known);
        }
        return known;
    }
    
    static class JarURLStreamHandler extends URLStreamHandler {

        private static final URLStreamHandler fallback = new URLStreamHandler() {
            protected @Override URLConnection openConnection(URL u) throws IOException {
                return new URL(u.toString()).openConnection();
            }
        };

        private final URLStreamHandler originalJarHandler;
        private ClassLoader loader;

        JarURLStreamHandler(URLStreamHandler originalJarHandler) {
            this.originalJarHandler = originalJarHandler;
        }

        private JarURLStreamHandler(ClassLoader l) {
            this(fallback);
            this.loader = l;
        }

        /**
         * Creates URLConnection for URL with res protocol.
         * @param u URL for which the URLConnection should be created
         * @return URLConnection
         * @throws IOException
         */
        @Override
        protected JarURLConnection openConnection(URL u) throws IOException {
            String url = u.getFile();//toExternalForm();
            int bang = url.indexOf("!/");
            if (bang == -1) {
                throw new IOException("Malformed JAR-protocol URL: " + u);
            }
            String filePath = url.substring(0, bang);
            String jar;
            AGAIN: for (;;) try {
                final URI uri = new URI(filePath);
                if (uri.getScheme().equals("file")) {
                    jar = BaseUtilities.toFile(uri).getPath();
                } else {
                    jar = null;
                }
                break;
            } catch (URISyntaxException x) {
                if (filePath.contains(" ")) {
                    filePath = filePath.replace(" ", "%20");
                    continue;
                }
                throw new IOException(x);
            }
            Source _src = Source.sources.get(jar);
            if (LOGGER.isLoggable(Level.FINE)) {
                LOGGER.log(Level.FINER, "openConnection for {0} jar: {1} src: {2}", new Object[]{u, jar, _src});
            }
            if (_src == null) {
                try {
                    Method m = URLStreamHandler.class.getDeclaredMethod("openConnection", URL.class);
                    m.setAccessible(true);
                    JarURLConnection ret = (JarURLConnection) m.invoke(originalJarHandler, u);
                    if (LOGGER.isLoggable(Level.FINER)) {
                        LOGGER.log(Level.FINER, "Calling original {0} yields {1}", new Object[]{originalJarHandler, ret});
                    }
                    return ret;
                } catch (Exception e) {
                    throw (IOException) new IOException(e.toString()).initCause(e);
                }
            }
            String _name = url.substring(bang + 2);
            try {
                _name = new URI(_name).getPath();
            } catch (URISyntaxException x) {
                throw (IOException) new IOException("Decoding " + u + ": " + x).initCause(x);
            }
            if (LOGGER.isLoggable(Level.FINER)) {
                LOGGER.log(Level.FINER, "creating NbJarURLConnection({0},{1},{2})", new Object[]{u, _src, _name});
            }
            return new NbJarURLConnection (u, _src, _name, loader);
        }
        
        @Override
        protected void parseURL(URL u, String spec, int start, int limit) {
            if (spec.startsWith("/")) {
                setURL(
                    u, "jar", u.getHost(), u.getPort(), 
                    u.getAuthority(), u.getUserInfo(), 
                    u.getFile().replaceFirst("!/.*$", "!" + spec), // NOI18N
                    u.getQuery(), u.getRef()
                ); 
            } else {
                super.parseURL(u, spec, start, limit);
            }
        }

    }

    /** URLConnection for URL with res protocol.
     *
     */
    private static class NbJarURLConnection extends JarURLConnection {
        private JarSource src;
        private final String name;
        private byte[] data;
        private InputStream iStream;
        private final ClassLoader loader;

        /**
         * Creates new URLConnection
         * @param url the parameter for which the connection should be
         * created
         */
        private NbJarURLConnection(URL url, Source src, String name, ClassLoader l) throws MalformedURLException {
            super(url);
            this.src = (JarSource)src;
            this.name = name;
            this.loader = l;
        }

        private boolean isFolder() {
            return name.length() == 0 || name.endsWith("/");
        }

        public void connect() throws IOException {
            if (isFolder()) {
                return; // #139087: odd but harmless
            }
            if (data == null) {
                data = src.getClassData(name);
                if (data == null) {
                    throw new FileNotFoundException(getURL().toString());
                }
            }
        }

        @Override
        public long getLastModified() {
            return Stamps.getModulesJARs().lastModified();
        }

        @Override
        public String getContentType() {
            String contentType = guessContentTypeFromName(name);
            if (contentType == null) {
                contentType = "content/unknown";
            }
            return contentType;
        }

        public @Override int getContentLength() {
            if (isFolder()) {
                return -1;
            }
            try {
                this.connect();
                return data.length;
            } catch (IOException e) {
                return -1;
            }
        }


        public @Override InputStream getInputStream() throws IOException {
            if (isFolder()) {
                throw new IOException("Cannot open a folder"); // NOI18N
            }
            this.connect();
            if (iStream == null) iStream = new ByteArrayInputStream(data);
            return iStream;
        }

        @Override
        public JarFile getJarFile() throws IOException {
            return new JarFile(src.file); // #134424
        }

        @SuppressWarnings("rawtypes")
        public @Override Object getContent(Class[] classes) throws IOException {
            if (Arrays.asList(classes).contains(ClassLoader.class)) {
                return loader;
            } else {
                return super.getContent(classes);
            }
        }
    }
}
