o.n.bootstrap/src/org/netbeans/JarClassLoader.java
author ffjre@netbeans.org
Wed Jul 30 09:13:02 2008 +0200 (4 weeks ago)
changeset 93285 3c52aa0be0ed
parent 93282e7036dc03341
parent 93272cb2a8e84db06
permissions -rw-r--r--
Automated merge
        1 /*
        2  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
        3  *
        4  * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
        5  *
        6  * The contents of this file are subject to the terms of either the GNU
        7  * General Public License Version 2 only ("GPL") or the Common
        8  * Development and Distribution License("CDDL") (collectively, the
        9  * "License"). You may not use this file except in compliance with the
       10  * License. You can obtain a copy of the License at
       11  * http://www.netbeans.org/cddl-gplv2.html
       12  * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
       13  * specific language governing permissions and limitations under the
       14  * License.  When distributing the software, include this License Header
       15  * Notice in each file and include the License file at
       16  * nbbuild/licenses/CDDL-GPL-2-CP.  Sun designates this
       17  * particular file as subject to the "Classpath" exception as provided
       18  * by Sun in the GPL Version 2 section of the License file that
       19  * accompanied this code. If applicable, add the following below the
       20  * License Header, with the fields enclosed by brackets [] replaced by
       21  * your own identifying information:
       22  * "Portions Copyrighted [year] [name of copyright owner]"
       23  *
       24  * Contributor(s):
       25  *
       26  * The Original Software is NetBeans. The Initial Developer of the Original
       27  * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun
       28  * Microsystems, Inc. All Rights Reserved.
       29  *
       30  * If you wish your version of this file to be governed by only the CDDL
       31  * or only the GPL Version 2, indicate your decision by adding
       32  * "[Contributor] elects to include this software in this distribution
       33  * under the [CDDL or GPL Version 2] license." If you do not indicate a
       34  * single choice of license, a recipient has the option to distribute
       35  * your version of this file under either the CDDL, the GPL Version 2 or
       36  * to extend the choice of license to its licensees as provided above.
       37  * However, if you add GPL Version 2 code and therefore, elected the GPL
       38  * Version 2 license, then the option applies only if the new code is
       39  * made subject to such option by the copyright holder.
       40  */
       41 
       42 package org.netbeans;
       43 
       44 import java.io.ByteArrayInputStream;
       45 import java.io.File;
       46 import java.io.FileInputStream;
       47 import java.io.FileNotFoundException;
       48 import java.io.FileOutputStream;
       49 import java.io.IOException;
       50 import java.io.InputStream;
       51 import java.io.OutputStream;
       52 import java.lang.reflect.Method;
       53 import java.net.JarURLConnection;
       54 import java.net.MalformedURLException;
       55 import java.net.URL;
       56 import java.net.URLStreamHandler;
       57 import java.security.CodeSource;
       58 import java.security.PermissionCollection;
       59 import java.security.Policy;
       60 import java.security.ProtectionDomain;
       61 import java.security.cert.Certificate;
       62 import java.util.ArrayList;
       63 import java.util.Arrays;
       64 import java.util.Collections;
       65 import java.util.Enumeration;
       66 import java.util.HashMap;
       67 import java.util.HashSet;
       68 import java.util.List;
       69 import java.util.Map;
       70 import java.util.Set;
       71 import java.util.Vector;
       72 import java.util.jar.Attributes;
       73 import java.util.jar.Attributes.Name;
       74 import java.util.jar.JarEntry;
       75 import java.util.jar.JarFile;
       76 import java.util.jar.Manifest;
       77 import java.util.logging.Level;
       78 import java.util.logging.Logger;
       79 import java.util.zip.ZipEntry;
       80 
       81 /**
       82  * A ProxyClassLoader capable of loading classes from a set of jar files
       83  * and local directories.
       84  *
       85  * @author  Petr Nejedly
       86  */
       87 public class JarClassLoader extends ProxyClassLoader {
       88     private static Stamps cache;
       89     static Archive archive = new Archive(); 
       90 
       91     static void initializeCache() {
       92         cache = Stamps.getModulesJARs();
       93         archive = new Archive(cache);
       94     }
       95     
       96     /**
       97      * Creates a new archive or updates existing archive with the necessary
       98      * resources gathered so far. It also stops gatheing and serving
       99      * additional request, if it was still doing so.
      100      */    
      101     public static void saveArchive() {
      102         archive.stopGathering();
      103         archive.stopServing();
      104         if (cache != null) {
      105             try {
      106                 archive.save(cache);
      107             } catch (IOException ioe) {
      108                 LOGGER.log(Level.WARNING, null, ioe);
      109             }
      110         }
      111     }
      112     
      113     static {
      114         ProxyURLStreamHandlerFactory.register();
      115     }
      116     
      117     private static final Logger LOGGER = Logger.getLogger(JarClassLoader.class.getName());
      118 
      119     private final Source[] sources;
      120     private Module module;
      121     
      122     /** Creates new JarClassLoader.
      123      * Gives transitive flag as true.
      124      */
      125     public JarClassLoader(List<File> files, ClassLoader[] parents) {
      126         this(files, parents, true, null);
      127     }
      128     
      129     public JarClassLoader(List<File> files, ClassLoader[] parents, boolean transitive) {
      130         this(files, parents, transitive, null);
      131     }
      132     /** Creates new JarClassLoader.
      133      * @since org.netbeans.core/1 > 1.6
      134      * @see ProxyClassLoader#ProxyClassLoader(ClassLoader[],boolean)
      135      */
      136     public JarClassLoader(List<File> files, ClassLoader[] parents, boolean transitive, Module mod) {
      137         super(parents, transitive);
      138         this.module = mod;
      139         List<Source> l = new ArrayList<Source>(files.size());
      140         try {
      141             for (File file : files) {
      142                 l.add(Source.create(file, this));
      143             }
      144         } catch (IOException exc) {
      145             throw new IllegalArgumentException(exc.getMessage());
      146         }
      147         sources = l.toArray(new Source[l.size()]);
      148         // overlaps with old packages doesn't matter,PCL uses sets.
      149         addCoveredPackages(getCoveredPackages(module, sources));
      150     }
      151 
      152     /** Allows to specify the right permissions, OneModuleClassLoader does it differently.
      153      */
      154     protected PermissionCollection getPermissions( CodeSource cs ) {           
      155         return Policy.getPolicy().getPermissions(cs);       
      156     }        
      157     
      158     
      159     protected Package definePackage(String name, Manifest man, URL url)
      160 	throws IllegalArgumentException
      161     {
      162         if (man == null ) {
      163             return definePackage(name, null, null, null, null, null, null, null);
      164         }
      165         
      166 	String path = name.replace('.', '/').concat("/"); // NOI18N
      167 	Attributes spec = man.getAttributes(path);
      168         Attributes main = man.getMainAttributes();
      169 	
      170         String specTitle = getAttr(spec, main, Name.SPECIFICATION_TITLE);
      171         String implTitle = getAttr(spec, main, Name.IMPLEMENTATION_TITLE);
      172         String specVersion = getAttr(spec, main, Name.SPECIFICATION_VERSION);
      173         String implVersion = getAttr(spec, main, Name.IMPLEMENTATION_VERSION);
      174         String specVendor = getAttr(spec, main, Name.SPECIFICATION_VENDOR);
      175         String implVendor = getAttr(spec, main, Name.IMPLEMENTATION_VENDOR);
      176         String sealed      = getAttr(spec, main, Name.SEALED);
      177 
      178         URL sealBase = "true".equalsIgnoreCase(sealed) ? url : null; // NOI18N
      179 	return definePackage(name, specTitle, specVersion, specVendor,
      180 			     implTitle, implVersion, implVendor, sealBase);
      181     }
      182 
      183     private static String getAttr(Attributes spec, Attributes main, Name name) {
      184         String val = null;
      185         if (spec != null) val = spec.getValue (name);
      186         if (val == null && main != null) val = main.getValue (name);
      187         return val;
      188     }
      189 
      190     @Override
      191     protected Class doLoadClass(String pkgName, String name) {
      192         String path = name.replace('.', '/').concat(".class"); // NOI18N
      193         
      194         // look up the Sources and return a class based on their content
      195         for( int i=0; i<sources.length; i++ ) {
      196             Source src = sources[i];
      197             byte[] data = src.getClassData(path);
      198             if (data == null) continue;
      199             
      200             // do the enhancing
      201             byte[] d = PatchByteCode.patch (data, name);
      202             data = d;
      203             
      204             // Note that we assume that if we are defining a class in this package,
      205             // we should also define the package! Thus recurse==false.
      206             // However special packages might be defined in a parent and then we want
      207             // to have the same Package object, proper sealing check, etc.; so be safe,
      208             // overhead is probably small (check in parents, nope, check super which
      209             // delegates to system loaders).
      210             Package pkg = getPackageFast(pkgName, true);
      211             if (pkg != null) {
      212                 // XXX full sealing check, URLClassLoader does something more
      213                 if (pkg.isSealed() && !pkg.isSealed(src.getURL())) throw new SecurityException("sealing violation"); // NOI18N
      214             } else {
      215                 Manifest man = module == null || src != sources[0] ? src.getManifest() : module.getManifest();
      216                 definePackage (pkgName, man, src.getURL());
      217             }
      218 
      219             return defineClass (name, data, 0, data.length, src.getProtectionDomain());
      220         } 
      221         return null;
      222     }
      223     // look up the jars and return a resource based on a content of jars
      224     @Override
      225     protected URL findResource(String name) {
      226         for( int i=0; i<sources.length; i++ ) {
      227             URL item = sources[i].getResource(name);
      228             if (item != null) return item;
      229         }
      230 	return null;
      231     }
      232 
      233     @Override
      234     protected Enumeration<URL> simpleFindResources(String name) {
      235         Vector<URL> v = new Vector<URL>(3);
      236         // look up the jars and return a resource based on a content of jars
      237 
      238         for( int i=0; i<sources.length; i++ ) {
      239             URL item = sources[i].getResource(name);
      240             if (item != null) v.add(item);
      241         }
      242         return v.elements();
      243     }
      244     
      245     public @Override void destroy() {
      246         super.destroy ();
      247         
      248         try {
      249             for (Source src : sources) src.destroy();
      250         } catch (IOException ioe) {
      251             LOGGER.log(Level.WARNING, null, ioe);
      252         }
      253     }
      254 
      255     static abstract class Source {
      256         private URL url;
      257         private ProtectionDomain pd;
      258         protected JarClassLoader jcl;
      259         private static Map<String,Source> sources = new HashMap<String, Source>();
      260         
      261         public Source(URL url) {
      262             this.url = url;
      263         }
      264         
      265         public final URL getURL() {
      266             return url;
      267         }
      268         
      269         public final ProtectionDomain getProtectionDomain() {
      270             if (pd == null) {
      271                 CodeSource cs = new CodeSource(url, (Certificate[])null);
      272                 pd = new ProtectionDomain(cs, jcl.getPermissions(cs));
      273             }
      274             return pd;
      275         }
      276   
      277         public final URL getResource(String name) {
      278             try {
      279                 return doGetResource(name);
      280             } catch (Exception e) {
      281                 // can't get the resource. E.g. already closed JarFile
      282                 LOGGER.log(Level.FINE, null, e);
      283             }
      284             return null;
      285         }
      286         
      287         protected abstract URL doGetResource(String name) throws IOException;
      288         
      289         public final byte[] getClassData(String path) {
      290             try {
      291                 return readClass(path);
      292             } catch (IOException e) {
      293                 LOGGER.log(Level.WARNING, null, e);
      294             }
      295             return null;
      296         }
      297 
      298         protected abstract byte[] readClass(String path) throws IOException;
      299 
      300         public Manifest getManifest() {
      301             return null;
      302         }
      303 
      304         protected abstract void listCoveredPackages(Set<String> known, StringBuffer save);
      305         
      306         protected void destroy() throws IOException {
      307             // relatively slow (millis instead of micros),
      308             // but rare enough to not matter
      309             sources.values().remove(this);
      310         }
      311         
      312         static Source create(File f, JarClassLoader jcl) throws IOException {
      313             Source src = f.isDirectory() ? new DirSource(f) : new JarSource(f);
      314             src.jcl = jcl;
      315             // should better use the same string as other indexes
      316             // this way, there are currently 3 similar long Strings per
      317             // JarClassLoader instance - its URL, its identifier
      318             // in Archive.sources map and this one
      319             sources.put(f.toURI().toString(), src);
      320             return src;
      321         }
      322 
      323         @Override
      324         public String toString() {
      325             return url.toString();
      326         }
      327 
      328     }
      329 
      330     static class JarSource extends Source {
      331         private String resPrefix;
      332         private File file;
      333 
      334         private JarFile jar;
      335         private boolean dead;
      336         private int requests;
      337         private int used;
      338         /** #141110: expensive to repeatedly look for them */
      339         private final Set<String> nonexistentResources = Collections.synchronizedSet(new HashSet<String>());
      340         
      341         JarSource(File file) throws IOException {
      342             this(file, "jar:" + file.toURI() + "!/"); // NOI18N
      343         }
      344         private JarSource(File file, String resPrefix) throws IOException {
      345             super(new URL(resPrefix)); // NOI18N
      346             this.resPrefix = resPrefix; // NOI18N;
      347             this.file = file;
      348         }
      349 
      350         @Override
      351         public Manifest getManifest() {
      352             try {
      353                 return getJarFile("man").getManifest();
      354             } catch (IOException e) {
      355                 return null;
      356             } finally {
      357                 releaseJarFile();
      358             }
      359         }
      360         
      361         JarFile getJarFile(String forWhat) throws IOException {
      362             synchronized(sources) {
      363                 requests++;
      364                 used++;
      365                 if (jar == null) {
      366                     jar = new JarFile(file, false);
      367                     opened(this, forWhat);
      368                 }
      369                 return jar;
      370             }
      371         }
      372         
      373         private void releaseJarFile() {
      374             synchronized(sources) {
      375                 assert used > 0;
      376                 used--;
      377             }
      378         }
      379         
      380         
      381         protected URL doGetResource(String name) throws IOException  {
      382             byte[] buf = archive.getData(this, name);
      383             if (buf == null) return null;
      384             LOGGER.log(Level.FINER, "Loading {0} from {1}", new Object[] {name, file.getPath()});
      385             return new URL(resPrefix + name);
      386         }
      387         
      388         protected byte[] readClass(String path) throws IOException {
      389             return archive.getData(this, path);
      390         }
      391         
      392         public byte[] resource(String path) throws IOException {
      393             if (nonexistentResources.contains(path)) {
      394                 return null;
      395             }
      396             ZipEntry ze;
      397             JarFile jf = getJarFile(path);
      398             try {
      399                 ze = jf.getEntry(path);
      400                 if (ze == null) {
      401                     nonexistentResources.add(path);
      402                     return null;
      403                 }
      404 
      405                 LOGGER.log(Level.FINER, "Loading {0} from {1}", new Object[] {path, file.getPath()});
      406             
      407                 int len = (int)ze.getSize();
      408                 byte[] data = new byte[len];
      409                 InputStream is = jf.getInputStream(ze);
      410                 int count = 0;
      411                 while (count < len) {
      412                     count += is.read(data, count, len-count);
      413                 }
      414                 return data;
      415             } finally {
      416                 releaseJarFile();
      417             }
      418         }
      419 
      420 
      421         protected void listCoveredPackages(Set<String> known, StringBuffer save) {
      422             try {
      423                 JarFile src = getJarFile("pkg");
      424 
      425                 Enumeration<JarEntry> en = src.entries();
      426                 while (en.hasMoreElements()) {
      427                     JarEntry je = en.nextElement();
      428                     if (! je.isDirectory()) {
      429                         String itm = je.getName();
      430                         int slash = itm.lastIndexOf('/');
      431                         String pkg = slash > 0 ? itm.substring(0, slash).replace('/','.') : "";
      432                         if (known.add(pkg)) save.append(pkg).append(',');
      433                         if (itm.startsWith("META-INF/")) {
      434                                 String res = itm.substring(8); // "/services/pkg.Service"
      435                                 if (known.add(res)) save.append(res).append(',');
      436                         }
      437                     }
      438                 }
      439             } catch (IOException ioe) {
      440                 LOGGER.log(Level.FINE, null, ioe);
      441             } finally {
      442                 releaseJarFile();
      443             }
      444         }
      445 
      446         
      447         @Override
      448         protected void destroy() throws IOException {
      449             super.destroy();
      450             assert dead == false : "Already had dead JAR: " + file;
      451             
      452             File orig = file;
      453 
      454             if (!orig.isFile()) {
      455                 // Can happen when a test module is deleted:
      456                 // the physical JAR has already been deleted
      457                 // when the module was disabled. In this case it
      458                 // is possible that a classloader request for something
      459                 // in the JAR could still come in. Does it matter?
      460                 // See comment in Module.cleanup.
      461                 return;
      462             }
      463             
      464             String name = orig.getName();
      465             String prefix, suffix;
      466             int idx = name.lastIndexOf('.');
      467             if (idx == -1) {
      468                 prefix = name;
      469                 suffix = null;
      470             } else {
      471                 prefix = name.substring(0, idx);
      472                 suffix = name.substring(idx);
      473             }
      474             
      475             while (prefix.length() < 3) prefix += "x"; // NOI18N
      476             File temp = File.createTempFile(prefix, suffix);
      477             temp.deleteOnExit();
      478 
      479             InputStream is = new FileInputStream(orig);
      480             try {
      481                 OutputStream os = new FileOutputStream(temp);
      482                 try {
      483                     byte[] buf = new byte[4096];
      484                     int j;
      485                     while ((j = is.read(buf)) != -1) {
      486                         os.write(buf, 0, j);
      487                     }
      488                 } finally {
      489                     os.close();
      490                 }
      491             } finally {
      492                 is.close();
      493             }
      494  
      495             doCloseJar();
      496             file = temp;
      497             dead = true;
      498             LOGGER.log(Level.FINE, "#21114: replacing {0} with {1}", new Object[] {orig, temp});
      499         }
      500         
      501         private void doCloseJar() throws IOException {
      502             synchronized(sources) {
      503                 if (jar != null) {
      504                     if (!sources.remove(this)) System.err.println("Can't remove " + this);
      505                     jar.close();
      506                     jar = null;
      507                 }
      508             }
      509             
      510         }
      511 
      512         /** Delete any temporary JARs we were holding on to.
      513          * Also close any other JARs in our list.
      514          */
      515         @Override
      516         protected void finalize() throws Throwable {
      517             super.finalize();
      518             
      519             doCloseJar();
      520 
      521             if (dead) {
      522                 LOGGER.log(Level.FINE, "#21114: closing and deleting temporary JAR {0}", file);
      523                 if (file.isFile() && !file.delete()) {
      524                     LOGGER.log(Level.FINE, "(but failed to delete {0})", file);
      525                 }
      526             }
      527         }
      528 
      529         // JarFile pool tracking
      530         private static final Set<JarSource> sources = new HashSet<JarSource>();
      531         private static int LIMIT = Integer.getInteger("org.netbeans.JarClassLoader.limit_fd", 300);
      532 
      533         static void opened(JarSource source, String forWhat) {
      534             synchronized (sources) {
      535                 assert !sources.contains(source) : "Failed for " + source.file.getPath() + "\n";
      536 
      537                 if (sources.size() > LIMIT) {
      538                     // close something
      539                     JarSource toClose = toClose();
      540                     try {
      541                         toClose.doCloseJar();
      542                     } catch (IOException ioe) {
      543                         LOGGER.log(Level.FINE, null, ioe);
      544                     }
      545                 }
      546                 
      547                 sources.add(source); // now register the newly opened
      548             }
      549         }
      550 
      551         // called under lock(sources) 
      552         private static JarSource toClose() { 
      553             assert Thread.holdsLock(sources); 
      554              
      555             int min = Integer.MAX_VALUE; 
      556             JarSource candidate = null; 
      557             for (JarSource act : sources) {
      558                 // aging: slight exponential decay of all opened sources?
      559                 act.requests = 5*act.requests/6;
      560                 
      561                 if (act.used > 0) continue;
      562                 if (act.requests < min) { 
      563                     min = act.requests; 
      564                     candidate = act; 
      565                 } 
      566             } 
      567              
      568             assert candidate != null; 
      569             return candidate; 
      570         }
      571 
      572         public String getIdentifier() {
      573             return getURL().toExternalForm();
      574         }
      575     }
      576 
      577     static class DirSource extends Source {
      578         File dir;
      579         
      580         DirSource(File file) throws MalformedURLException {
      581             super(file.toURI().toURL());
      582             dir = file;
      583         }
      584 
      585         protected URL doGetResource(String name) throws MalformedURLException {
      586             File resFile = new File(dir, name);
      587             return resFile.exists() ? resFile.toURI().toURL() : null;
      588         }
      589         
      590         protected byte[] readClass(String path) throws IOException {
      591             File clsFile = new File(dir, path.replace('/', File.separatorChar));
      592             if (!clsFile.exists()) return null;
      593             
      594             int len = (int)clsFile.length();
      595             byte[] data = new byte[len];
      596             InputStream is = new FileInputStream(clsFile);
      597             try {
      598                 int count = 0;
      599                 while (count < len) {
      600                     count += is.read(data, count, len - count);
      601                 }
      602                 return data;
      603             } finally {
      604                 is.close();
      605             }
      606         }
      607         
      608         protected void listCoveredPackages(Set<String> known, StringBuffer save) {
      609             appendAllChildren(known, save, dir, "");
      610         }
      611         
      612         private static void appendAllChildren(Set<String> known, StringBuffer save, File dir, String prefix) {
      613             boolean populated = false;
      614             for (File f : dir.listFiles()) {
      615                 if (f.isDirectory()) {
      616                     appendAllChildren(known, save, f, prefix + f.getName() + '.');
      617                 } else {
      618                     populated = true;
      619                     if (prefix.startsWith("META-INF.")) {
      620                        String res = prefix.substring(8).replace('.', '/').concat(f.getName());
      621                        if (known.add(res)) save.append(res).append(',');
      622                     }
      623                 }
      624             }
      625             if (populated) {
      626                 String pkg = prefix;
      627                 if (pkg.endsWith(".")) pkg = pkg.substring(0, pkg.length()-1);
      628                 if (known.add(pkg)) save.append(pkg).append(',');
      629             }
      630         }
      631     }
      632     
      633     private static Iterable<String> getCoveredPackages(Module mod, Source[] sources) {
      634         Set<String> known = new HashSet<String>();
      635         Manifest m = mod == null ? null : mod.getManifest();
      636         if (m != null) {
      637             Attributes attr = m.getMainAttributes();
      638             String pack = attr.getValue("Covered-Packages");
      639             if (pack != null) {
      640                 known.addAll(Arrays.asList(pack.split(",", -1)));
      641                 return known;
      642             }
      643         }
      644         
      645         // not precomputed/cached, analyze
      646         StringBuffer save = new StringBuffer();
      647         for (Source s : sources) s.listCoveredPackages(known, save);
      648 
      649         if (save.length() > 0) save.setLength(save.length()-1);
      650         if (m != null) {
      651             Attributes attr = m.getMainAttributes();
      652             attr.putValue("Covered-Packages", save.toString());
      653         }
      654         return known;
      655     }
      656     
      657     /**
      658      * URLStreamHandler for res protocol
      659      */
      660     static class ResURLStreamHandler extends URLStreamHandler {
      661 
      662         private final URLStreamHandler originalJarHandler;
      663 
      664         ResURLStreamHandler(URLStreamHandler originalJarHandler) {
      665             this.originalJarHandler = originalJarHandler;
      666         }
      667 
      668         /**
      669          * Creates URLConnection for URL with res protocol.
      670          * @param u URL for which the URLConnection should be created
      671          * @return URLConnection
      672          * @throws IOException
      673          */
      674         protected JarURLConnection openConnection(URL u) throws IOException {
      675             String url = u.getFile();//toExternalForm();
      676             int bang = url.indexOf("!/");
      677             if (bang == -1) {
      678                 throw new IOException("Malformed JAR-protocol URL: " + u);
      679             }
      680             String jar = url.substring(0, bang);
      681             String _name = url.substring(bang+2);
      682             Source _src = Source.sources.get(jar);
      683             if (_src == null) {
      684                 try {
      685                     Method m = URLStreamHandler.class.getDeclaredMethod("openConnection", URL.class);
      686                     m.setAccessible(true);
      687                     return (JarURLConnection) m.invoke(originalJarHandler, u);
      688                 } catch (Exception e) {
      689                     throw (IOException) new IOException(e.toString()).initCause(e);
      690                 }
      691             }
      692             return new ResURLConnection (u, _src, _name);
      693         }
      694 
      695         @Override
      696         protected void parseURL(URL u, String spec, int start, int limit) {
      697             if (spec.startsWith("/")) {
      698                 setURL(u, "jar", null, 0, null, null, u.getFile().replaceFirst("!/.+$", "!" + spec), u.getQuery(), u.getRef()); // NOI18N
      699             } else {
      700                 super.parseURL(u, spec, start, limit);
      701             }
      702         }
      703 
      704     }
      705 
      706     /** URLConnection for URL with res protocol.
      707      *
      708      */
      709     private static class ResURLConnection extends JarURLConnection {
      710         private JarSource src;
      711         private final String name;
      712         private byte[] data;
      713         private InputStream iStream;
      714 
      715         /**
      716          * Creates new URLConnection
      717          * @param url the parameter for which the connection should be
      718          * created
      719          */
      720         private ResURLConnection(URL url, Source src, String name) throws MalformedURLException {
      721             super(url);
      722             this.src = (JarSource)src;
      723             this.name = name;
      724         }
      725 
      726         private boolean isFolder() {
      727             return name.length() == 0 || name.endsWith("/");
      728         }
      729 
      730         public void connect() throws IOException {
      731             if (isFolder()) {
      732                 return; // #139087: odd but harmless
      733             }
      734             if (data == null) {
      735                 data = src.getClassData(name);
      736                 if (data == null) {
      737                     throw new FileNotFoundException(getURL().toString());
      738                 }
      739             }
      740         }
      741 
      742         @Override
      743         public String getContentType() {
      744             String contentType = guessContentTypeFromName(name);
      745             if (contentType == null) {
      746                 contentType = "content/unknown";
      747             }
      748             return contentType;
      749         }
      750 
      751         public @Override int getContentLength() {
      752             if (isFolder()) {
      753                 return -1;
      754             }
      755             try {
      756                 this.connect();
      757                 return data.length;
      758             } catch (IOException e) {
      759                 return -1;
      760             }
      761         }
      762 
      763 
      764         public @Override InputStream getInputStream() throws IOException {
      765             if (isFolder()) {
      766                 throw new IOException("Cannot open a folder"); // NOI18N
      767             }
      768             this.connect();
      769             if (iStream == null) iStream = new ByteArrayInputStream(data);
      770             return iStream;
      771         }
      772 
      773         @Override
      774         public JarFile getJarFile() throws IOException {
      775             return new JarFile(src.file); // #134424
      776