001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans;
043:
044: import java.io.ByteArrayInputStream;
045: import java.io.File;
046: import java.io.FileInputStream;
047: import java.io.FileNotFoundException;
048: import java.io.FileOutputStream;
049: import java.io.IOException;
050: import java.io.InputStream;
051: import java.io.OutputStream;
052: import java.lang.reflect.Field;
053: import java.net.MalformedURLException;
054: import java.net.URL;
055: import java.net.URLConnection;
056: import java.net.URLStreamHandler;
057: import java.net.URLStreamHandlerFactory;
058: import java.security.CodeSource;
059: import java.security.PermissionCollection;
060: import java.security.Policy;
061: import java.security.ProtectionDomain;
062: import java.security.cert.Certificate;
063: import java.util.ArrayList;
064: import java.util.Arrays;
065: import java.util.Enumeration;
066: import java.util.HashMap;
067: import java.util.HashSet;
068: import java.util.List;
069: import java.util.Map;
070: import java.util.Set;
071: import java.util.Vector;
072: import java.util.jar.Attributes;
073: import java.util.jar.Attributes.Name;
074: import java.util.jar.JarEntry;
075: import java.util.jar.JarFile;
076: import java.util.jar.Manifest;
077: import java.util.logging.Level;
078: import java.util.logging.Logger;
079: import java.util.zip.ZipEntry;
080:
081: /**
082: * A ProxyClassLoader capable of loading classes from a set of jar files
083: * and local directories.
084: *
085: * @author Petr Nejedly
086: */
087: public class JarClassLoader extends ProxyClassLoader {
088: private static Stamps cache;
089: static Archive archive = new Archive();
090:
091: static void initializeCache() {
092: cache = Stamps.getModulesJARs();
093: archive = new Archive(cache);
094: }
095:
096: /**
097: * Creates a new archive or updates existing archive with the necessary
098: * resources gathered so far. It also stops gatheing and serving
099: * 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: ResURLStreamHandlerFactory fact = new ResURLStreamHandlerFactory();
115: try {
116: java.net.URL.setURLStreamHandlerFactory(fact);
117: } catch (Error e) {
118: try {
119: for (Field f : URL.class.getDeclaredFields()) {
120: if (f.getType() == URLStreamHandlerFactory.class) {
121: f.setAccessible(true);
122: fact.del = (URLStreamHandlerFactory) f
123: .get(null);
124: f.set(null, null);
125: break;
126: }
127: }
128: URL.setURLStreamHandlerFactory(fact);
129: } catch (Throwable t) {
130: throw new InternalError(); // can't really continue
131: }
132: }
133: }
134:
135: private static final Logger LOGGER = Logger
136: .getLogger(JarClassLoader.class.getName());
137:
138: private Source[] sources = new Source[0];
139: private Module module;
140:
141: /** Creates new JarClassLoader.
142: * Gives transitive flag as true.
143: */
144: public JarClassLoader(List<File> files, ClassLoader[] parents) {
145: this (files, parents, true, null);
146: }
147:
148: public JarClassLoader(List<File> files, ClassLoader[] parents,
149: boolean transitive) {
150: this (files, parents, transitive, null);
151: }
152:
153: /** Creates new JarClassLoader.
154: * @since org.netbeans.core/1 > 1.6
155: * @see ProxyClassLoader#ProxyClassLoader(ClassLoader[],boolean)
156: */
157: public JarClassLoader(List<File> files, ClassLoader[] parents,
158: boolean transitive, Module mod) {
159: super (parents, transitive);
160: this .module = mod;
161: addSources(files);
162: }
163:
164: /** Boot classloader needs to add entries for netbeans.user later.
165: */
166: final void addSources(List<File> newSources) {
167: ArrayList<Source> l = new ArrayList<Source>(sources.length
168: + newSources.size());
169: l.addAll(Arrays.asList(sources));
170: try {
171: for (File file : newSources) {
172: l.add(Source.create(file, this ));
173: }
174: } catch (IOException exc) {
175: throw new IllegalArgumentException(exc.getMessage());
176: }
177: sources = l.toArray(sources);
178: // overlaps with old packages doesn't matter,PCL uses sets.
179: addCoveredPackages(getCoveredPackages(module, sources));
180: }
181:
182: /** Allows to specify the right permissions, OneModuleClassLoader does it differently.
183: */
184: protected PermissionCollection getPermissions(CodeSource cs) {
185: return Policy.getPolicy().getPermissions(cs);
186: }
187:
188: protected Package definePackage(String name, Manifest man, URL url)
189: throws IllegalArgumentException {
190: if (man == null) {
191: return definePackage(name, null, null, null, null, null,
192: null, null);
193: }
194:
195: String path = name.replace('.', '/').concat("/"); // NOI18N
196: Attributes spec = man.getAttributes(path);
197: Attributes main = man.getMainAttributes();
198:
199: String specTitle = getAttr(spec, main, Name.SPECIFICATION_TITLE);
200: String implTitle = getAttr(spec, main,
201: Name.IMPLEMENTATION_TITLE);
202: String specVersion = getAttr(spec, main,
203: Name.SPECIFICATION_VERSION);
204: String implVersion = getAttr(spec, main,
205: Name.IMPLEMENTATION_VERSION);
206: String specVendor = getAttr(spec, main,
207: Name.SPECIFICATION_VENDOR);
208: String implVendor = getAttr(spec, main,
209: Name.IMPLEMENTATION_VENDOR);
210: String sealed = getAttr(spec, main, Name.SEALED);
211:
212: URL sealBase = "true".equalsIgnoreCase(sealed) ? url : null; // NOI18N
213: return definePackage(name, specTitle, specVersion, specVendor,
214: implTitle, implVersion, implVendor, sealBase);
215: }
216:
217: private static String getAttr(Attributes spec, Attributes main,
218: Name name) {
219: String val = null;
220: if (spec != null)
221: val = spec.getValue(name);
222: if (val == null && main != null)
223: val = main.getValue(name);
224: return val;
225: }
226:
227: @Override
228: protected Class doLoadClass(String pkgName, String name) {
229: String path = name.replace('.', '/').concat(".class"); // NOI18N
230:
231: // look up the Sources and return a class based on their content
232: for (int i = 0; i < sources.length; i++) {
233: Source src = sources[i];
234: byte[] data = src.getClassData(path);
235: if (data == null)
236: continue;
237:
238: // do the enhancing
239: byte[] d = PatchByteCode.patch(data, name);
240: data = d;
241:
242: // Note that we assume that if we are defining a class in this package,
243: // we should also define the package! Thus recurse==false.
244: // However special packages might be defined in a parent and then we want
245: // to have the same Package object, proper sealing check, etc.; so be safe,
246: // overhead is probably small (check in parents, nope, check super which
247: // delegates to system loaders).
248: Package pkg = getPackageFast(pkgName, true);
249: if (pkg != null) {
250: // XXX full sealing check, URLClassLoader does something more
251: if (pkg.isSealed() && !pkg.isSealed(src.getURL()))
252: throw new SecurityException("sealing violation"); // NOI18N
253: } else {
254: Manifest man = module == null || src != sources[0] ? src
255: .getManifest()
256: : module.getManifest();
257: definePackage(pkgName, man, src.getURL());
258: }
259:
260: return defineClass(name, data, 0, data.length, src
261: .getProtectionDomain());
262: }
263: return null;
264: }
265:
266: // look up the jars and return a resource based on a content of jars
267: @Override
268: protected URL findResource(String name) {
269: for (int i = 0; i < sources.length; i++) {
270: URL item = sources[i].getResource(name);
271: if (item != null)
272: return item;
273: }
274: return null;
275: }
276:
277: @Override
278: protected Enumeration<URL> simpleFindResources(String name) {
279: Vector<URL> v = new Vector<URL>(3);
280: // look up the jars and return a resource based on a content of jars
281:
282: for (int i = 0; i < sources.length; i++) {
283: URL item = sources[i].getResource(name);
284: if (item != null)
285: v.add(item);
286: }
287: return v.elements();
288: }
289:
290: public @Override
291: void destroy() {
292: super .destroy();
293:
294: try {
295: for (Source src : sources)
296: src.destroy();
297: } catch (IOException ioe) {
298: LOGGER.log(Level.WARNING, null, ioe);
299: }
300: }
301:
302: static abstract class Source {
303: private URL url;
304: private ProtectionDomain pd;
305: protected JarClassLoader jcl;
306: private static Map<String, Source> sources = new HashMap<String, Source>();
307:
308: public Source(URL url) {
309: this .url = url;
310: }
311:
312: public final URL getURL() {
313: return url;
314: }
315:
316: public final ProtectionDomain getProtectionDomain() {
317: if (pd == null) {
318: CodeSource cs = new CodeSource(url,
319: (Certificate[]) null);
320: pd = new ProtectionDomain(cs, jcl.getPermissions(cs));
321: }
322: return pd;
323: }
324:
325: public final URL getResource(String name) {
326: try {
327: return doGetResource(name);
328: } catch (Exception e) {
329: // can't get the resource. E.g. already closed JarFile
330: LOGGER.log(Level.FINE, null, e);
331: }
332: return null;
333: }
334:
335: protected abstract URL doGetResource(String name)
336: throws IOException;
337:
338: public final byte[] getClassData(String path) {
339: try {
340: return readClass(path);
341: } catch (IOException e) {
342: LOGGER.log(Level.FINE, null, e);
343: }
344: return null;
345: }
346:
347: protected abstract byte[] readClass(String path)
348: throws IOException;
349:
350: public Manifest getManifest() {
351: return null;
352: }
353:
354: protected abstract void listCoveredPackages(Set<String> known,
355: StringBuffer save);
356:
357: protected void destroy() throws IOException {
358: // relatively slow (millis instead of micros),
359: // but rare enough to not matter
360: sources.values().remove(this );
361: }
362:
363: static Source create(File f, JarClassLoader jcl)
364: throws IOException {
365: Source src = f.isDirectory() ? new DirSource(f)
366: : new JarSource(f);
367: src.jcl = jcl;
368: // should better use the same string as other indexes
369: // this way, there are currently 3 similar long Strings per
370: // JarClassLoader instance - its URL, its identifier
371: // in Archive.sources map and this one
372: sources.put(f.toURI().toString(), src);
373: return src;
374: }
375: }
376:
377: static class JarSource extends Source {
378: private String resPrefix;
379: private File file;
380:
381: private JarFile jar;
382: private boolean dead;
383: private int requests;
384: private int used;
385:
386: JarSource(File file) throws IOException {
387: super (file.toURL());
388: resPrefix = ResURLStreamHandler.RES_PROTO + ":"
389: + file.toURI() + "!/"; // NOI18N;
390: this .file = file;
391: }
392:
393: @Override
394: public Manifest getManifest() {
395: try {
396: return getJarFile("man").getManifest();
397: } catch (IOException e) {
398: return null;
399: } finally {
400: releaseJarFile();
401: }
402: }
403:
404: JarFile getJarFile(String forWhat) throws IOException {
405: synchronized (sources) {
406: requests++;
407: used++;
408: if (jar == null) {
409: jar = new JarFile(file, false);
410: opened(this , forWhat);
411: }
412: return jar;
413: }
414: }
415:
416: private void releaseJarFile() {
417: synchronized (sources) {
418: assert used > 0;
419: used--;
420: }
421: }
422:
423: protected URL doGetResource(String name) throws IOException {
424: byte[] buf = archive.getData(this , name);
425: if (buf == null)
426: return null;
427: LOGGER.log(Level.FINER, "Loading {0} from {1}",
428: new Object[] { name, file.getPath() });
429: return new URL(resPrefix + name);
430: }
431:
432: protected byte[] readClass(String path) throws IOException {
433: return archive.getData(this , path);
434: }
435:
436: public byte[] resource(String path) throws IOException {
437: ZipEntry ze;
438: JarFile jf = getJarFile(path);
439: try {
440: ze = jf.getEntry(path);
441: if (ze == null)
442: return null;
443:
444: LOGGER.log(Level.FINER, "Loading {0} from {1}",
445: new Object[] { path, file.getPath() });
446:
447: int len = (int) ze.getSize();
448: byte[] data = new byte[len];
449: InputStream is = jf.getInputStream(ze);
450: int count = 0;
451: while (count < len) {
452: count += is.read(data, count, len - count);
453: }
454: return data;
455: } catch (IllegalStateException ex) {
456: // this exception occurs in org/netbeans/core/lookup/* tests
457: // without this catch statement the tests fail
458: return null;
459: } finally {
460: releaseJarFile();
461: }
462: }
463:
464: protected void listCoveredPackages(Set<String> known,
465: StringBuffer save) {
466: try {
467: JarFile src = getJarFile("pkg");
468:
469: Enumeration<JarEntry> en = src.entries();
470: while (en.hasMoreElements()) {
471: JarEntry je = en.nextElement();
472: if (!je.isDirectory()) {
473: String itm = je.getName();
474: int slash = itm.lastIndexOf('/');
475: String pkg = slash > 0 ? itm
476: .substring(0, slash).replace('/', '.')
477: : "";
478: if (known.add(pkg))
479: save.append(pkg).append(',');
480: if (itm.startsWith("META-INF/")) {
481: String res = itm.substring(8); // "/services/pkg.Service"
482: if (known.add(res))
483: save.append(res).append(',');
484: }
485: }
486: }
487: } catch (IOException ioe) {
488: LOGGER.log(Level.FINE, null, ioe);
489: } finally {
490: releaseJarFile();
491: }
492: }
493:
494: @Override
495: protected void destroy() throws IOException {
496: super .destroy();
497: assert dead == false : "Already had dead JAR: " + file;
498:
499: File orig = file;
500:
501: if (!orig.isFile()) {
502: // Can happen when a test module is deleted:
503: // the physical JAR has already been deleted
504: // when the module was disabled. In this case it
505: // is possible that a classloader request for something
506: // in the JAR could still come in. Does it matter?
507: // See comment in Module.cleanup.
508: return;
509: }
510:
511: String name = orig.getName();
512: String prefix, suffix;
513: int idx = name.lastIndexOf('.');
514: if (idx == -1) {
515: prefix = name;
516: suffix = null;
517: } else {
518: prefix = name.substring(0, idx);
519: suffix = name.substring(idx);
520: }
521:
522: while (prefix.length() < 3)
523: prefix += "x"; // NOI18N
524: File temp = File.createTempFile(prefix, suffix);
525: temp.deleteOnExit();
526:
527: InputStream is = new FileInputStream(orig);
528: try {
529: OutputStream os = new FileOutputStream(temp);
530: try {
531: byte[] buf = new byte[4096];
532: int j;
533: while ((j = is.read(buf)) != -1) {
534: os.write(buf, 0, j);
535: }
536: } finally {
537: os.close();
538: }
539: } finally {
540: is.close();
541: }
542:
543: doCloseJar();
544: file = temp;
545: dead = true;
546: LOGGER.log(Level.FINE, "#21114: replacing {0} with {1}",
547: new Object[] { orig, temp });
548: }
549:
550: private void doCloseJar() throws IOException {
551: synchronized (sources) {
552: if (jar != null) {
553: if (!sources.remove(this ))
554: System.err.println("Can't remove " + this );
555: jar.close();
556: jar = null;
557: }
558: }
559:
560: }
561:
562: /** Delete any temporary JARs we were holding on to.
563: * Also close any other JARs in our list.
564: */
565: @Override
566: protected void finalize() throws Throwable {
567: super .finalize();
568:
569: doCloseJar();
570:
571: if (dead) {
572: LOGGER
573: .log(
574: Level.FINE,
575: "#21114: closing and deleting temporary JAR {0}",
576: file);
577: if (file.isFile() && !file.delete()) {
578: LOGGER.log(Level.FINE,
579: "(but failed to delete {0})", file);
580: }
581: }
582: }
583:
584: // JarFile pool tracking
585: private static Set<JarSource> sources = new HashSet<JarSource>();
586: private static int LIMIT = Integer.getInteger(
587: "org.netbeans.JarClassLoader.limit_fd", 300);
588:
589: static void opened(JarSource source, String forWhat) {
590: synchronized (sources) {
591: assert !sources.contains(source) : "Failed for "
592: + source.file.getPath() + "\n";
593:
594: if (sources.size() > LIMIT) {
595: // close something
596: JarSource toClose = toClose();
597: try {
598: toClose.doCloseJar();
599: } catch (IOException ioe) {
600: LOGGER.log(Level.FINE, null, ioe);
601: }
602: }
603:
604: sources.add(source); // now register the newly opened
605: }
606: }
607:
608: // called under lock(sources)
609: private static JarSource toClose() {
610: assert Thread.holdsLock(sources);
611:
612: int min = Integer.MAX_VALUE;
613: JarSource candidate = null;
614: for (JarSource act : sources) {
615: // aging: slight exponential decay of all opened sources?
616: act.requests = 5 * act.requests / 6;
617:
618: if (act.used > 0)
619: continue;
620: if (act.requests < min) {
621: min = act.requests;
622: candidate = act;
623: }
624: }
625:
626: assert candidate != null;
627: return candidate;
628: }
629:
630: public String getIdentifier() {
631: return getURL().toExternalForm();
632: }
633: }
634:
635: static class DirSource extends Source {
636: File dir;
637:
638: DirSource(File file) throws MalformedURLException {
639: super (file.toURL());
640: dir = file;
641: }
642:
643: protected URL doGetResource(String name)
644: throws MalformedURLException {
645: File resFile = new File(dir, name);
646: return resFile.exists() ? resFile.toURI().toURL() : null;
647: }
648:
649: protected byte[] readClass(String path) throws IOException {
650: File clsFile = new File(dir, path.replace('/',
651: File.separatorChar));
652: if (!clsFile.exists())
653: return null;
654:
655: int len = (int) clsFile.length();
656: byte[] data = new byte[len];
657: InputStream is = new FileInputStream(clsFile);
658: try {
659: int count = 0;
660: while (count < len) {
661: count += is.read(data, count, len - count);
662: }
663: return data;
664: } finally {
665: is.close();
666: }
667: }
668:
669: protected void listCoveredPackages(Set<String> known,
670: StringBuffer save) {
671: appendAllChildren(known, save, dir, "");
672: }
673:
674: private static void appendAllChildren(Set<String> known,
675: StringBuffer save, File dir, String prefix) {
676: boolean populated = false;
677: for (File f : dir.listFiles()) {
678: if (f.isDirectory()) {
679: appendAllChildren(known, save, f, prefix
680: + f.getName() + '.');
681: } else {
682: populated = true;
683: if (prefix.startsWith("META-INF.")) {
684: String res = prefix.substring(8).replace('.',
685: '/').concat(f.getName());
686: if (known.add(res))
687: save.append(res).append(',');
688: }
689: }
690: }
691: if (populated) {
692: String pkg = prefix;
693: if (pkg.endsWith("."))
694: pkg = pkg.substring(0, pkg.length() - 1);
695: if (known.add(pkg))
696: save.append(pkg).append(',');
697: }
698: }
699: }
700:
701: private static Iterable<String> getCoveredPackages(Module mod,
702: Source[] sources) {
703: Set<String> known = new HashSet<String>();
704: Manifest m = mod == null ? null : mod.getManifest();
705: if (m != null) {
706: Attributes attr = m.getMainAttributes();
707: String pack = attr.getValue("Covered-Packages");
708: if (pack != null) {
709: known.addAll(Arrays.asList(pack.split(",", -1)));
710: return known;
711: }
712: }
713:
714: // not precomputed/cached, analyze
715: StringBuffer save = new StringBuffer();
716: for (Source s : sources)
717: s.listCoveredPackages(known, save);
718:
719: if (save.length() > 0)
720: save.setLength(save.length() - 1);
721: if (m != null) {
722: Attributes attr = m.getMainAttributes();
723: attr.putValue("Covered-Packages", save.toString());
724: }
725: return known;
726: }
727:
728: private static class ResURLStreamHandlerFactory implements
729: URLStreamHandlerFactory {
730: URLStreamHandlerFactory del;
731:
732: /**
733: * Creates URLStreamHandler for nbinst protocol
734: * @param protocol
735: * @return NbinstURLStreamHandler if the protocol is nbinst otherwise null
736: */
737: public URLStreamHandler createURLStreamHandler(String protocol) {
738: if (ResURLStreamHandler.RES_PROTO.equals(protocol)) {
739: return new ResURLStreamHandler();
740: }
741: return del != null ? del.createURLStreamHandler(protocol)
742: : null;
743: }
744: }
745:
746: /**
747: * URLStreamHandler for res protocol
748: */
749: private static class ResURLStreamHandler extends URLStreamHandler {
750: public static final String RES_PROTO = "nbjcl";
751:
752: ResURLStreamHandler() {
753: }
754:
755: /**
756: * Creates URLConnection for URL with res protocol.
757: * @param u URL for which the URLConnection should be created
758: * @return URLConnection
759: * @throws IOException
760: */
761: protected URLConnection openConnection(URL u)
762: throws IOException {
763: String url = u.getFile();//toExternalForm();
764: int bang = url.indexOf("!/");
765: String jar = url.substring(0, bang);
766: String _name = url.substring(bang + 2);
767: Source _src = Source.sources.get(jar);
768: if (_src == null) {
769: String replace = u.toExternalForm().replaceAll("nbjcl",
770: "jar");
771:
772: if (archive.isActive()) {
773: LOGGER.log(Level.WARNING,
774: "Cannot find {0} in current sources", jar);
775: if (LOGGER.isLoggable(Level.FINER)) {
776: LOGGER.log(Level.FINER, dumpSources(
777: Source.sources, jar));
778: }
779: LOGGER.log(Level.WARNING, "Trying {0} instead",
780: replace);
781: LOGGER.log(Level.WARNING, "Disabling class cache");
782: archive.stopServing();
783: }
784: return new URL(replace).openConnection();
785: }
786: return new ResURLConnection(u, _src, _name);
787: }
788:
789: protected @Override
790: void parseURL(URL url, String spec, int start, int limit) {
791: String file = null;
792: String ref = null;
793: // first figure out if there is an anchor
794: int refPos = spec.indexOf('#', limit);
795: boolean refOnly = refPos == start;
796: if (refPos > -1) {
797: ref = spec.substring(refPos + 1, spec.length());
798: if (refOnly) {
799: file = url.getFile();
800: }
801: }
802: // then figure out if the spec is
803: // 1. absolute (res:)
804: // 2. relative (i.e. url + foo/bar/baz.ext)
805: // 3. anchor-only (i.e. url + #foo), which we already did (refOnly)
806: boolean absoluteSpec = false;
807: if (spec.length() >= RES_PROTO.length() + 1) {
808: absoluteSpec = spec
809: .substring(0, RES_PROTO.length() + 1)
810: .equalsIgnoreCase(RES_PROTO + ":");
811: }
812: spec = spec.substring(start, limit);
813:
814: if (absoluteSpec) {
815: file = parseAbsoluteSpec(spec);
816: } else if (!refOnly) {
817: file = parseContextSpec(url, spec);
818:
819: // Canonize the result after the bangslash
820: int bangSlash = file.lastIndexOf("!/") + 1;
821: String toBangSlash = file.substring(0, bangSlash);
822: String afterBangSlash = file.substring(bangSlash);
823: sun.net.www.ParseUtil canonizer = new sun.net.www.ParseUtil(); // XXX
824: afterBangSlash = canonizer
825: .canonizeString(afterBangSlash);
826: file = toBangSlash + afterBangSlash;
827: }
828: setURLOK(url, file, ref);
829: }
830:
831: private static String dumpSources(Map<String, Source> sources,
832: String jar) {
833: StringBuilder sb = new StringBuilder();
834: sb.append("Searching for ").append(jar).append(
835: "\nwhile available:\n");
836: for (Map.Entry<String, Source> entry : sources.entrySet()) {
837: sb.append(entry.getKey()).append('\n');
838: }
839:
840: return sb.toString();
841: }
842:
843: @SuppressWarnings("deprecation")
844: private void setURLOK(URL url, String file, String ref) {
845: super .setURL(url, RES_PROTO, "", -1, file, ref);
846: }
847:
848: private String parseAbsoluteSpec(String spec) {
849: URL url = null;
850: int index = -1;
851: // check for !/
852: if ((index = spec.lastIndexOf("!/") + 1) == -1) {
853: throw new NullPointerException("no !/ in spec");
854: }
855: // test the inner URL
856: try {
857: String innerSpec = spec.substring(0, index - 1);
858: url = new URL(innerSpec);
859: } catch (MalformedURLException e) {
860: throw new NullPointerException("invalid url: " + spec
861: + " (" + e + ")");
862: }
863: return spec;
864: }
865:
866: private String parseContextSpec(URL url, String spec) {
867: String ctxFile = url.getFile();
868: // if the spec begins with /, chop up the jar back !/
869: if (spec.startsWith("/")) {
870: int bangSlash = ctxFile.lastIndexOf("!/");
871: if (bangSlash == -1) {
872: throw new NullPointerException("malformed "
873: + "context url:" + url + ": no !/");
874: }
875: ctxFile = ctxFile.substring(0, bangSlash + 1);
876: }
877: if (!ctxFile.endsWith("/") && (!spec.startsWith("/"))) {
878: // chop up the last component
879: int lastSlash = ctxFile.lastIndexOf('/');
880: if (lastSlash == -1) {
881: throw new NullPointerException("malformed "
882: + "context url:" + url);
883: }
884: ctxFile = ctxFile.substring(0, lastSlash + 1);
885: }
886: return (ctxFile + spec);
887: }
888: }
889:
890: /** URLConnection for URL with res protocol.
891: *
892: */
893: private static class ResURLConnection extends URLConnection {
894: private JarSource src;
895: private String name;
896: private byte[] data;
897: private InputStream iStream;
898:
899: /**
900: * Creates new URLConnection
901: * @param url the parameter for which the connection should be
902: * created
903: */
904: private ResURLConnection(URL url, Source src, String name) {
905: super (url);
906: this .src = (JarSource) src;
907: this .name = name;
908: }
909:
910: public void connect() throws IOException {
911: if (data == null) {
912: data = src.getClassData(name);
913: if (data == null) {
914: throw new FileNotFoundException(getURL().toString());
915: }
916: }
917: }
918:
919: @Override
920: public String getContentType() {
921: String contentType = guessContentTypeFromName(name);
922: if (contentType == null) {
923: contentType = "content/unknown";
924: }
925: return contentType;
926: }
927:
928: public @Override
929: int getContentLength() {
930: try {
931: this .connect();
932: return data.length;
933: } catch (IOException e) {
934: return -1;
935: }
936: }
937:
938: public @Override
939: InputStream getInputStream() throws IOException {
940: this .connect();
941: if (iStream == null)
942: iStream = new ByteArrayInputStream(data);
943: return iStream;
944: }
945: }
946: }
|