2 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
4 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
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]"
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.
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.
44 import java.io.ByteArrayInputStream;
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;
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;
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;
82 * A ProxyClassLoader capable of loading classes from a set of jar files
83 * and local directories.
85 * @author Petr Nejedly
87 public class JarClassLoader extends ProxyClassLoader {
88 private static Stamps cache;
89 static Archive archive = new Archive();
91 static void initializeCache() {
92 cache = Stamps.getModulesJARs();
93 archive = new Archive(cache);
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.
101 public static void saveArchive() {
102 archive.stopGathering();
103 archive.stopServing();
107 } catch (IOException ioe) {
108 LOGGER.log(Level.WARNING, null, ioe);
114 ProxyURLStreamHandlerFactory.register();
117 private static final Logger LOGGER = Logger.getLogger(JarClassLoader.class.getName());
119 private final Source[] sources;
120 private Module module;
122 /** Creates new JarClassLoader.
123 * Gives transitive flag as true.
125 public JarClassLoader(List<File> files, ClassLoader[] parents) {
126 this(files, parents, true, null);
129 public JarClassLoader(List<File> files, ClassLoader[] parents, boolean transitive) {
130 this(files, parents, transitive, null);
132 /** Creates new JarClassLoader.
133 * @since org.netbeans.core/1 > 1.6
134 * @see ProxyClassLoader#ProxyClassLoader(ClassLoader[],boolean)
136 public JarClassLoader(List<File> files, ClassLoader[] parents, boolean transitive, Module mod) {
137 super(parents, transitive);
139 List<Source> l = new ArrayList<Source>(files.size());
141 for (File file : files) {
142 l.add(Source.create(file, this));
144 } catch (IOException exc) {
145 throw new IllegalArgumentException(exc.getMessage());
147 sources = l.toArray(new Source[l.size()]);
148 // overlaps with old packages doesn't matter,PCL uses sets.
149 addCoveredPackages(getCoveredPackages(module, sources));
152 /** Allows to specify the right permissions, OneModuleClassLoader does it differently.
154 protected PermissionCollection getPermissions( CodeSource cs ) {
155 return Policy.getPolicy().getPermissions(cs);
159 protected Package definePackage(String name, Manifest man, URL url)
160 throws IllegalArgumentException
163 return definePackage(name, null, null, null, null, null, null, null);
166 String path = name.replace('.', '/').concat("/"); // NOI18N
167 Attributes spec = man.getAttributes(path);
168 Attributes main = man.getMainAttributes();
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);
178 URL sealBase = "true".equalsIgnoreCase(sealed) ? url : null; // NOI18N
179 return definePackage(name, specTitle, specVersion, specVendor,
180 implTitle, implVersion, implVendor, sealBase);
183 private static String getAttr(Attributes spec, Attributes main, Name name) {
185 if (spec != null) val = spec.getValue (name);
186 if (val == null && main != null) val = main.getValue (name);
191 protected Class doLoadClass(String pkgName, String name) {
192 String path = name.replace('.', '/').concat(".class"); // NOI18N
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;
201 byte[] d = PatchByteCode.patch (data, name);
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);
212 // XXX full sealing check, URLClassLoader does something more
213 if (pkg.isSealed() && !pkg.isSealed(src.getURL())) throw new SecurityException("sealing violation"); // NOI18N
215 Manifest man = module == null || src != sources[0] ? src.getManifest() : module.getManifest();
216 definePackage (pkgName, man, src.getURL());
219 return defineClass (name, data, 0, data.length, src.getProtectionDomain());
223 // look up the jars and return a resource based on a content of jars
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;
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
238 for( int i=0; i<sources.length; i++ ) {
239 URL item = sources[i].getResource(name);
240 if (item != null) v.add(item);
245 public @Override void destroy() {
249 for (Source src : sources) src.destroy();
250 } catch (IOException ioe) {
251 LOGGER.log(Level.WARNING, null, ioe);
255 static abstract class Source {
257 private ProtectionDomain pd;
258 protected JarClassLoader jcl;
259 private static Map<String,Source> sources = new HashMap<String, Source>();
261 public Source(URL url) {
265 public final URL getURL() {
269 public final ProtectionDomain getProtectionDomain() {
271 CodeSource cs = new CodeSource(url, (Certificate[])null);
272 pd = new ProtectionDomain(cs, jcl.getPermissions(cs));
277 public final URL getResource(String name) {
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);
287 protected abstract URL doGetResource(String name) throws IOException;
289 public final byte[] getClassData(String path) {
291 return readClass(path);
292 } catch (IOException e) {
293 LOGGER.log(Level.WARNING, null, e);
298 protected abstract byte[] readClass(String path) throws IOException;
300 public Manifest getManifest() {
304 protected abstract void listCoveredPackages(Set<String> known, StringBuffer save);
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);
312 static Source create(File f, JarClassLoader jcl) throws IOException {
313 Source src = f.isDirectory() ? new DirSource(f) : new JarSource(f);
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);
324 public String toString() {
325 return url.toString();
330 static class JarSource extends Source {
331 private String resPrefix;
335 private boolean dead;
336 private int requests;
338 /** #141110: expensive to repeatedly look for them */
339 private final Set<String> nonexistentResources = Collections.synchronizedSet(new HashSet<String>());
341 JarSource(File file) throws IOException {
342 this(file, "jar:" + file.toURI() + "!/"); // NOI18N
344 private JarSource(File file, String resPrefix) throws IOException {
345 super(new URL(resPrefix)); // NOI18N
346 this.resPrefix = resPrefix; // NOI18N;
351 public Manifest getManifest() {
353 return getJarFile("man").getManifest();
354 } catch (IOException e) {
361 JarFile getJarFile(String forWhat) throws IOException {
362 synchronized(sources) {
366 jar = new JarFile(file, false);
367 opened(this, forWhat);
373 private void releaseJarFile() {
374 synchronized(sources) {
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);
388 protected byte[] readClass(String path) throws IOException {
389 return archive.getData(this, path);
392 public byte[] resource(String path) throws IOException {
393 if (nonexistentResources.contains(path)) {
397 JarFile jf = getJarFile(path);
399 ze = jf.getEntry(path);
401 nonexistentResources.add(path);
405 LOGGER.log(Level.FINER, "Loading {0} from {1}", new Object[] {path, file.getPath()});
407 int len = (int)ze.getSize();
408 byte[] data = new byte[len];
409 InputStream is = jf.getInputStream(ze);
411 while (count < len) {
412 count += is.read(data, count, len-count);
421 protected void listCoveredPackages(Set<String> known, StringBuffer save) {
423 JarFile src = getJarFile("pkg");
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(',');
439 } catch (IOException ioe) {
440 LOGGER.log(Level.FINE, null, ioe);
448 protected void destroy() throws IOException {
450 assert dead == false : "Already had dead JAR: " + file;
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.
464 String name = orig.getName();
465 String prefix, suffix;
466 int idx = name.lastIndexOf('.');
471 prefix = name.substring(0, idx);
472 suffix = name.substring(idx);
475 while (prefix.length() < 3) prefix += "x"; // NOI18N
476 File temp = File.createTempFile(prefix, suffix);
479 InputStream is = new FileInputStream(orig);
481 OutputStream os = new FileOutputStream(temp);
483 byte[] buf = new byte[4096];
485 while ((j = is.read(buf)) != -1) {
498 LOGGER.log(Level.FINE, "#21114: replacing {0} with {1}", new Object[] {orig, temp});
501 private void doCloseJar() throws IOException {
502 synchronized(sources) {
504 if (!sources.remove(this)) System.err.println("Can't remove " + this);
512 /** Delete any temporary JARs we were holding on to.
513 * Also close any other JARs in our list.
516 protected void finalize() throws Throwable {
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);
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);
533 static void opened(JarSource source, String forWhat) {
534 synchronized (sources) {
535 assert !sources.contains(source) : "Failed for " + source.file.getPath() + "\n";
537 if (sources.size() > LIMIT) {
539 JarSource toClose = toClose();
541 toClose.doCloseJar();
542 } catch (IOException ioe) {
543 LOGGER.log(Level.FINE, null, ioe);
547 sources.add(source); // now register the newly opened
551 // called under lock(sources)
552 private static JarSource toClose() {
553 assert Thread.holdsLock(sources);
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;
561 if (act.used > 0) continue;
562 if (act.requests < min) {
568 assert candidate != null;
572 public String getIdentifier() {
573 return getURL().toExternalForm();
577 static class DirSource extends Source {
580 DirSource(File file) throws MalformedURLException {
581 super(file.toURI().toURL());
585 protected URL doGetResource(String name) throws MalformedURLException {
586 File resFile = new File(dir, name);
587 return resFile.exists() ? resFile.toURI().toURL() : null;
590 protected byte[] readClass(String path) throws IOException {
591 File clsFile = new File(dir, path.replace('/', File.separatorChar));
592 if (!clsFile.exists()) return null;
594 int len = (int)clsFile.length();
595 byte[] data = new byte[len];
596 InputStream is = new FileInputStream(clsFile);
599 while (count < len) {
600 count += is.read(data, count, len - count);
608 protected void listCoveredPackages(Set<String> known, StringBuffer save) {
609 appendAllChildren(known, save, dir, "");
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() + '.');
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(',');
627 if (pkg.endsWith(".")) pkg = pkg.substring(0, pkg.length()-1);
628 if (known.add(pkg)) save.append(pkg).append(',');
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();
637 Attributes attr = m.getMainAttributes();
638 String pack = attr.getValue("Covered-Packages");
640 known.addAll(Arrays.asList(pack.split(",", -1)));
645 // not precomputed/cached, analyze
646 StringBuffer save = new StringBuffer();
647 for (Source s : sources) s.listCoveredPackages(known, save);
649 if (save.length() > 0) save.setLength(save.length()-1);
651 Attributes attr = m.getMainAttributes();
652 attr.putValue("Covered-Packages", save.toString());
658 * URLStreamHandler for res protocol
660 static class ResURLStreamHandler extends URLStreamHandler {
662 private final URLStreamHandler originalJarHandler;
664 ResURLStreamHandler(URLStreamHandler originalJarHandler) {
665 this.originalJarHandler = originalJarHandler;
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
674 protected JarURLConnection openConnection(URL u) throws IOException {
675 String url = u.getFile();//toExternalForm();
676 int bang = url.indexOf("!/");
678 throw new IOException("Malformed JAR-protocol URL: " + u);
680 String jar = url.substring(0, bang);
681 String _name = url.substring(bang+2);
682 Source _src = Source.sources.get(jar);
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);
692 return new ResURLConnection (u, _src, _name);
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
700 super.parseURL(u, spec, start, limit);
706 /** URLConnection for URL with res protocol.
709 private static class ResURLConnection extends JarURLConnection {
710 private JarSource src;
711 private final String name;
713 private InputStream iStream;
716 * Creates new URLConnection
717 * @param url the parameter for which the connection should be
720 private ResURLConnection(URL url, Source src, String name) throws MalformedURLException {
722 this.src = (JarSource)src;
726 private boolean isFolder() {
727 return name.length() == 0 || name.endsWith("/");
730 public void connect() throws IOException {
732 return; // #139087: odd but harmless
735 data = src.getClassData(name);
737 throw new FileNotFoundException(getURL().toString());
743 public String getContentType() {
744 String contentType = guessContentTypeFromName(name);
745 if (contentType == null) {
746 contentType = "content/unknown";
751 public @Override int getContentLength() {
758 } catch (IOException e) {
764 public @Override InputStream getInputStream() throws IOException {
766 throw new IOException("Cannot open a folder"); // NOI18N
769 if (iStream == null) iStream = new ByteArrayInputStream(data);
774 public JarFile getJarFile() throws IOException {
775 return new JarFile(src.file); // #134424