001: /*
002: * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved
003: *
004: * This file is part of Resin(R) Open Source
005: *
006: * Each copy or derived work must preserve the copyright notice and this
007: * notice unmodified.
008: *
009: * Resin Open Source is free software; you can redistribute it and/or modify
010: * it under the terms of the GNU General Public License as published by
011: * the Free Software Foundation; either version 2 of the License, or
012: * (at your option) any later version.
013: *
014: * Resin Open Source is distributed in the hope that it will be useful,
015: * but WITHOUT ANY WARRANTY; without even the implied warranty of
016: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
017: * of NON-INFRINGEMENT. See the GNU General Public License for more
018: * details.
019: *
020: * You should have received a copy of the GNU General Public License
021: * along with Resin Open Source; if not, write to the
022: *
023: * Free Software Foundation, Inc.
024: * 59 Temple Place, Suite 330
025: * Boston, MA 02111-1307 USA
026: *
027: * @author Scott Ferguson
028: */
029:
030: package com.caucho.vfs;
031:
032: import com.caucho.loader.EnvironmentLocal;
033: import com.caucho.make.CachedDependency;
034: import com.caucho.util.Alarm;
035: import com.caucho.util.CacheListener;
036: import com.caucho.util.Log;
037: import com.caucho.util.LruCache;
038: import com.caucho.util.L10N;
039:
040: import java.io.FileNotFoundException;
041: import java.io.IOException;
042: import java.io.InputStream;
043: import java.lang.ref.SoftReference;
044: import java.security.cert.Certificate;
045: import java.util.*;
046: import java.util.jar.*;
047: import java.util.logging.Level;
048: import java.util.logging.Logger;
049: import java.util.zip.ZipEntry;
050: import java.util.zip.ZipFile;
051: import java.util.zip.ZipInputStream;
052:
053: /**
054: * Jar is a cache around a jar file to avoid scanning through the whole
055: * file on each request.
056: *
057: * <p>When the Jar is created, it scans the file and builds a directory
058: * of the Jar entries.
059: */
060: public class Jar implements CacheListener {
061: private static final Logger log = Log.open(Jar.class);
062: private static final L10N L = new L10N(Jar.class);
063:
064: private static LruCache<Path, Jar> _jarCache;
065:
066: private static EnvironmentLocal<Integer> _jarSize = new EnvironmentLocal<Integer>(
067: "caucho.vfs.jar-size");
068:
069: private Path _backing;
070: private boolean _backingIsFile;
071:
072: private JarDepend _depend;
073:
074: // saved last modified time
075: private long _lastModified;
076: // saved length
077: private long _length;
078: // last time the file was checked
079: private long _lastTime;
080:
081: // cached zip file to read jar entries
082: private SoftReference<JarFile> _jarFileRef;
083: // last time the zip file was modified
084: private long _jarLastModified;
085: private long _jarLength;
086: private Boolean _isSigned;
087:
088: // file to be closed
089: private SoftReference<JarFile> _closeJarFileRef;
090:
091: /**
092: * Creates a new Jar.
093: *
094: * @param path canonical path
095: */
096: private Jar(Path backing) {
097: if (backing instanceof JarPath)
098: throw new IllegalStateException();
099:
100: _backing = backing;
101:
102: _backingIsFile = (_backing.getScheme().equals("file") && _backing
103: .canRead());
104: }
105:
106: /**
107: * Return a Jar for the path. If the backing already exists, return
108: * the old jar.
109: */
110: static Jar create(Path backing) {
111: if (_jarCache == null) {
112: int size = 256;
113:
114: Integer iSize = _jarSize.get();
115:
116: if (iSize != null)
117: size = iSize.intValue();
118:
119: _jarCache = new LruCache<Path, Jar>(size);
120: }
121:
122: Jar jar = _jarCache.get(backing);
123: if (jar == null) {
124: jar = new Jar(backing);
125: jar = _jarCache.putIfNew(backing, jar);
126: }
127:
128: return jar;
129: }
130:
131: /**
132: * Return a Jar for the path. If the backing already exists, return
133: * the old jar.
134: */
135: static Jar getJar(Path backing) {
136: if (_jarCache != null) {
137: Jar jar = _jarCache.get(backing);
138:
139: return jar;
140: }
141:
142: return null;
143: }
144:
145: /**
146: * Return a Jar for the path. If the backing already exists, return
147: * the old jar.
148: */
149: public static PersistentDependency createDepend(Path backing) {
150: Jar jar = create(backing);
151:
152: return jar.getDepend();
153: }
154:
155: /**
156: * Return a Jar for the path. If the backing already exists, return
157: * the old jar.
158: */
159: public static PersistentDependency createDepend(Path backing,
160: long digest) {
161: Jar jar = create(backing);
162:
163: return new JarDigestDepend(jar.getJarDepend(), digest);
164: }
165:
166: /**
167: * Returns the backing path.
168: */
169: Path getBacking() {
170: return _backing;
171: }
172:
173: /**
174: * Returns the dependency.
175: */
176: public PersistentDependency getDepend() {
177: return getJarDepend();
178: }
179:
180: /**
181: * Returns the dependency.
182: */
183: private JarDepend getJarDepend() {
184: if (_depend == null || _depend.isModified())
185: _depend = new JarDepend(new Depend(getBacking()));
186:
187: return _depend;
188: }
189:
190: private boolean isSigned() {
191: Boolean isSigned = _isSigned;
192:
193: if (isSigned != null)
194: return isSigned;
195:
196: try {
197: Manifest manifest = getManifest();
198:
199: if (manifest == null) {
200: _isSigned = Boolean.FALSE;
201: return false;
202: }
203:
204: Map<String, Attributes> entries = manifest.getEntries();
205:
206: if (entries == null) {
207: _isSigned = Boolean.FALSE;
208: return false;
209: }
210:
211: for (Attributes attr : entries.values()) {
212: for (Object key : attr.keySet()) {
213: String keyString = String.valueOf(key);
214:
215: if (keyString.contains("Digest")) {
216: _isSigned = Boolean.TRUE;
217:
218: return true;
219: }
220: }
221: }
222: } catch (IOException e) {
223: log.log(Level.FINE, e.toString(), e);
224: }
225:
226: _isSigned = Boolean.FALSE;
227:
228: return false;
229: }
230:
231: /**
232: * Returns true if the entry is a file in the jar.
233: *
234: * @param path the path name inside the jar.
235: */
236: public Manifest getManifest() throws IOException {
237: Manifest manifest;
238:
239: synchronized (this ) {
240: JarFile jarFile = getJarFile();
241:
242: if (jarFile == null)
243: manifest = null;
244: else
245: manifest = jarFile.getManifest();
246: }
247:
248: closeJarFile();
249:
250: return manifest;
251: }
252:
253: /**
254: * Returns any certificates.
255: */
256: public Certificate[] getCertificates(String path) {
257: if (!isSigned())
258: return null;
259:
260: if (path.length() > 0 && path.charAt(0) == '/')
261: path = path.substring(1);
262:
263: try {
264: if (!_backing.canRead())
265: return null;
266:
267: JarFile jarFile = new JarFile(_backing.getNativePath());
268: JarEntry entry;
269: InputStream is = null;
270:
271: try {
272: entry = jarFile.getJarEntry(path);
273:
274: if (entry != null) {
275: is = jarFile.getInputStream(entry);
276:
277: while (is.skip(65536) > 0) {
278: }
279:
280: is.close();
281:
282: return entry.getCertificates();
283: }
284: } finally {
285: jarFile.close();
286: }
287: } catch (IOException e) {
288: log.log(Level.FINE, e.toString(), e);
289:
290: return null;
291: }
292:
293: return null;
294: }
295:
296: /**
297: * Returns true if the entry exists in the jar.
298: *
299: * @param path the path name inside the jar.
300: */
301: public boolean exists(String path) {
302: // server/249f, server/249g
303: // XXX: facelets vs issue of meta-inf (i.e. lower case)
304:
305: boolean result = false;
306:
307: synchronized (this ) {
308: try {
309: ZipEntry entry = getJarEntry(path);
310:
311: result = entry != null;
312: } catch (IOException e) {
313: log.log(Level.FINE, e.toString(), e);
314: }
315: }
316:
317: closeJarFile();
318:
319: return result;
320: }
321:
322: /**
323: * Returns true if the entry is a directory in the jar.
324: *
325: * @param path the path name inside the jar.
326: */
327: public boolean isDirectory(String path) {
328: boolean result = false;
329:
330: synchronized (this ) {
331: try {
332: ZipEntry entry = getJarEntry(path);
333:
334: result = entry != null && entry.isDirectory();
335: } catch (IOException e) {
336: log.log(Level.FINE, e.toString(), e);
337: }
338: }
339:
340: closeJarFile();
341:
342: return result;
343: }
344:
345: /**
346: * Returns true if the entry is a file in the jar.
347: *
348: * @param path the path name inside the jar.
349: */
350: public boolean isFile(String path) {
351: boolean result = false;
352:
353: synchronized (this ) {
354: try {
355: ZipEntry entry = getJarEntry(path);
356:
357: result = entry != null && !entry.isDirectory();
358: } catch (IOException e) {
359: log.log(Level.FINE, e.toString(), e);
360: }
361: }
362:
363: closeJarFile();
364:
365: return result;
366: }
367:
368: /**
369: * Returns the length of the entry in the jar file.
370: *
371: * @param path full path to the jar entry
372: * @return the length of the entry
373: */
374: public long getLastModified(String path) {
375: long result = -1;
376: synchronized (this ) {
377: try {
378: ZipEntry entry = getJarEntry(path);
379:
380: result = entry != null ? entry.getTime() : -1;
381: } catch (IOException e) {
382: log.log(Level.FINE, e.toString(), e);
383: }
384: }
385:
386: closeJarFile();
387:
388: return result;
389: }
390:
391: /**
392: * Returns the length of the entry in the jar file.
393: *
394: * @param path full path to the jar entry
395: * @return the length of the entry
396: */
397: public long getLength(String path) {
398: long result = -1;
399:
400: synchronized (this ) {
401: try {
402: ZipEntry entry = getJarEntry(path);
403:
404: result = entry != null ? entry.getSize() : -1;
405: } catch (IOException e) {
406: log.log(Level.FINE, e.toString(), e);
407: }
408: }
409:
410: closeJarFile();
411:
412: return result;
413: }
414:
415: /**
416: * Readable if the jar is readable and the path refers to a file.
417: */
418: public boolean canRead(String path) {
419: boolean result = false;
420:
421: synchronized (this ) {
422: try {
423: ZipEntry entry = getJarEntry(path);
424:
425: result = entry != null && !entry.isDirectory();
426: } catch (IOException e) {
427: log.log(Level.FINE, e.toString(), e);
428:
429: return false;
430: }
431: }
432:
433: closeJarFile();
434:
435: return result;
436: }
437:
438: /**
439: * Can't write to jars.
440: */
441: public boolean canWrite(String path) {
442: return false;
443: }
444:
445: /**
446: * Lists all the files in this directory.
447: */
448: public String[] list(String path) throws IOException {
449: // XXX:
450:
451: return new String[0];
452: }
453:
454: /**
455: * Opens a stream to an entry in the jar.
456: *
457: * @param path relative path into the jar.
458: */
459: public StreamImpl openReadImpl(Path path) throws IOException {
460: String pathName = path.getPath();
461:
462: if (pathName.length() > 0 && pathName.charAt(0) == '/')
463: pathName = pathName.substring(1);
464:
465: ZipFile zipFile = new ZipFile(_backing.getNativePath());
466: ZipEntry entry;
467: InputStream is = null;
468:
469: try {
470: entry = zipFile.getEntry(pathName);
471: if (entry != null) {
472: is = zipFile.getInputStream(entry);
473:
474: return new ZipStreamImpl(zipFile, is, null, path);
475: } else {
476: throw new FileNotFoundException(path.toString());
477: }
478: } finally {
479: if (is == null) {
480: zipFile.close();
481: }
482: }
483: }
484:
485: public String toString() {
486: return _backing.toString();
487: }
488:
489: /**
490: * Clears any cached JarFile.
491: */
492: public void clearCache() {
493: JarFile jarFile = null;
494:
495: synchronized (this ) {
496: SoftReference<JarFile> jarFileRef = _jarFileRef;
497: _jarFileRef = null;
498:
499: if (jarFileRef != null)
500: jarFile = jarFileRef.get();
501: }
502:
503: try {
504: if (jarFile != null)
505: jarFile.close();
506: } catch (Exception e) {
507: }
508: }
509:
510: private ZipEntry getJarEntry(String path) throws IOException {
511: if (path.startsWith("/"))
512: path = path.substring(1);
513:
514: JarFile jarFile = getJarFile();
515:
516: if (jarFile != null)
517: return jarFile.getJarEntry(path);
518: else
519: return null;
520: }
521:
522: /**
523: * Returns the Java ZipFile for this Jar. Accessing the entries with
524: * the ZipFile is faster than scanning through them.
525: *
526: * getJarFile is not thread safe.
527: */
528: private JarFile getJarFile() throws IOException {
529: JarFile jarFile = null;
530:
531: isCacheValid();
532:
533: SoftReference<JarFile> jarFileRef = _jarFileRef;
534:
535: if (jarFileRef != null) {
536: jarFile = jarFileRef.get();
537:
538: if (jarFile != null)
539: return jarFile;
540: }
541:
542: SoftReference<JarFile> oldJarRef = _jarFileRef;
543: _jarFileRef = null;
544:
545: JarFile oldFile = null;
546: if (oldJarRef == null) {
547: } else if (_closeJarFileRef == null)
548: _closeJarFileRef = oldJarRef;
549: else
550: oldFile = oldJarRef.get();
551:
552: if (oldFile != null) {
553: try {
554: oldFile.close();
555: } catch (Throwable e) {
556: e.printStackTrace();
557: }
558: }
559:
560: if (_backingIsFile) {
561: try {
562: jarFile = new JarFile(_backing.getNativePath());
563: } catch (IOException ex) {
564: if (log.isLoggable(Level.FINE))
565: log.log(Level.FINE, L.l(
566: "Error opening jar file '{0}'", _backing
567: .getNativePath()));
568:
569: throw ex;
570: }
571:
572: _jarFileRef = new SoftReference<JarFile>(jarFile);
573: _jarLastModified = getLastModifiedImpl();
574: }
575:
576: return jarFile;
577: }
578:
579: /**
580: * Returns the last modified time for the path.
581: *
582: * @param path path into the jar.
583: *
584: * @return the last modified time of the jar in milliseconds.
585: */
586: private long getLastModifiedImpl() {
587: isCacheValid();
588:
589: return _lastModified;
590: }
591:
592: /**
593: * Returns the last modified time for the path.
594: *
595: * @param path path into the jar.
596: *
597: * @return the last modified time of the jar in milliseconds.
598: */
599: private boolean isCacheValid() {
600: long now = Alarm.getCurrentTime();
601:
602: if (now == _lastTime && !Alarm.isTest())
603: return true;
604:
605: long oldLastModified = _lastModified;
606: long oldLength = _length;
607:
608: _lastModified = _backing.getLastModified();
609: _length = _backing.getLength();
610: _lastTime = now;
611:
612: if (_lastModified == oldLastModified && _length == oldLength)
613: return true;
614: else {
615: // If the file has changed, close the old file
616: SoftReference<JarFile> oldFileRef = _jarFileRef;
617:
618: _jarFileRef = null;
619: _jarLastModified = 0;
620: _depend = null;
621: _isSigned = null;
622:
623: JarFile oldCloseFile = null;
624: if (_closeJarFileRef != null)
625: oldCloseFile = _closeJarFileRef.get();
626:
627: _closeJarFileRef = oldFileRef;
628:
629: if (oldCloseFile != null) {
630: try {
631: oldCloseFile.close();
632: } catch (Throwable e) {
633: }
634: }
635:
636: return false;
637: }
638: }
639:
640: /**
641: * Closes any old jar waiting for close.
642: */
643: private void closeJarFile() {
644: JarFile jarFile = null;
645:
646: synchronized (this ) {
647: if (_closeJarFileRef != null)
648: jarFile = _closeJarFileRef.get();
649: _closeJarFileRef = null;
650: }
651:
652: if (jarFile != null) {
653: try {
654: jarFile.close();
655: } catch (IOException e) {
656: log.log(Level.WARNING, e.toString(), e);
657: }
658: }
659: }
660:
661: public void close() {
662: removeEvent();
663: }
664:
665: public void removeEvent() {
666: JarFile jarFile = null;
667:
668: synchronized (this ) {
669: if (_jarFileRef != null)
670: jarFile = _jarFileRef.get();
671:
672: _jarFileRef = null;
673: }
674:
675: try {
676: if (jarFile != null)
677: jarFile.close();
678: } catch (Throwable e) {
679: log.log(Level.FINE, e.toString(), e);
680: }
681:
682: closeJarFile();
683: }
684:
685: public boolean equals(Object o) {
686: if (this == o)
687: return true;
688: else if (o == null || !getClass().equals(o.getClass()))
689: return false;
690:
691: Jar jar = (Jar) o;
692:
693: return _backing.equals(jar._backing);
694: }
695:
696: /**
697: * Clears all the cached files in the jar. Needed to avoid some
698: * windows NT issues.
699: */
700: public static void clearJarCache() {
701: LruCache<Path, Jar> jarCache = _jarCache;
702:
703: if (jarCache == null)
704: return;
705:
706: ArrayList<Jar> jars = new ArrayList<Jar>();
707:
708: synchronized (jarCache) {
709: Iterator<Jar> iter = jarCache.values();
710:
711: while (iter.hasNext())
712: jars.add(iter.next());
713: }
714:
715: for (int i = 0; i < jars.size(); i++) {
716: Jar jar = jars.get(i);
717:
718: if (jar != null)
719: jar.clearCache();
720: }
721: }
722:
723: /**
724: * StreamImpl to read from a ZIP file.
725: */
726: static class ZipStreamImpl extends StreamImpl {
727: private ZipFile _zipFile;
728: private InputStream _zis;
729: private InputStream _is;
730:
731: /**
732: * Create the new stream impl.
733: *
734: * @param zis the underlying zip stream.
735: * @param is the backing stream.
736: * @param path the path to the jar entry.
737: */
738: ZipStreamImpl(ZipFile file, InputStream zis, InputStream is,
739: Path path) {
740: _zipFile = file;
741: _zis = zis;
742: _is = is;
743:
744: setPath(path);
745: }
746:
747: /**
748: * Returns true since this is a read stream.
749: */
750: public boolean canRead() {
751: return true;
752: }
753:
754: public int getAvailable() throws IOException {
755: if (_zis == null)
756: return -1;
757: else
758: return _zis.available();
759: }
760:
761: public int read(byte[] buf, int off, int len)
762: throws IOException {
763: int readLen = _zis.read(buf, off, len);
764:
765: return readLen;
766: }
767:
768: public void close() throws IOException {
769: ZipFile zipFile = _zipFile;
770: _zipFile = null;
771:
772: InputStream zis = _zis;
773: _zis = null;
774:
775: InputStream is = _is;
776: _is = null;
777:
778: try {
779: if (zis != null)
780: zis.close();
781: } catch (Throwable e) {
782: }
783:
784: try {
785: if (zipFile != null)
786: zipFile.close();
787: } catch (Throwable e) {
788: }
789:
790: if (is != null)
791: is.close();
792: }
793:
794: protected void finalize() throws IOException {
795: close();
796: }
797: }
798:
799: static class JarDepend extends CachedDependency implements
800: PersistentDependency {
801: private Depend _depend;
802: private boolean _isDigestModified;
803:
804: /**
805: * Create a new dependency.
806: *
807: * @param source the source file
808: */
809: JarDepend(Depend depend) {
810: _depend = depend;
811: }
812:
813: /**
814: * Create a new dependency.
815: *
816: * @param source the source file
817: */
818: JarDepend(Depend depend, long digest) {
819: _depend = depend;
820:
821: _isDigestModified = _depend.getDigest() != digest;
822: }
823:
824: /**
825: * Returns the underlying depend.
826: */
827: Depend getDepend() {
828: return _depend;
829: }
830:
831: /**
832: * Returns true if the dependency is modified.
833: */
834: public boolean isModifiedImpl() {
835: return _isDigestModified || _depend.isModified();
836: }
837:
838: /**
839: * Returns true if the dependency is modified.
840: */
841: public boolean logModified(Logger log) {
842: return _depend.logModified(log);
843: }
844:
845: /**
846: * Returns the string to recreate the Dependency.
847: */
848: public String getJavaCreateString() {
849: String sourcePath = _depend.getPath().getPath();
850: long digest = _depend.getDigest();
851:
852: return ("new com.caucho.vfs.Jar.createDepend("
853: + "com.caucho.vfs.Vfs.lookup(\"" + sourcePath
854: + "\"), " + digest + "L)");
855: }
856:
857: public String toString() {
858: return "Jar$JarDepend[" + _depend.getPath() + "]";
859: }
860: }
861:
862: static class JarDigestDepend implements PersistentDependency {
863: private JarDepend _jarDepend;
864: private Depend _depend;
865: private boolean _isDigestModified;
866:
867: /**
868: * Create a new dependency.
869: *
870: * @param source the source file
871: */
872: JarDigestDepend(JarDepend jarDepend, long digest) {
873: _jarDepend = jarDepend;
874: _depend = jarDepend.getDepend();
875:
876: _isDigestModified = _depend.getDigest() != digest;
877: }
878:
879: /**
880: * Returns true if the dependency is modified.
881: */
882: public boolean isModified() {
883: return _isDigestModified || _jarDepend.isModified();
884: }
885:
886: /**
887: * Returns true if the dependency is modified.
888: */
889: public boolean logModified(Logger log) {
890: return _depend.logModified(log)
891: || _jarDepend.logModified(log);
892: }
893:
894: /**
895: * Returns the string to recreate the Dependency.
896: */
897: public String getJavaCreateString() {
898: String sourcePath = _depend.getPath().getPath();
899: long digest = _depend.getDigest();
900:
901: return ("new com.caucho.vfs.Jar.createDepend("
902: + "com.caucho.vfs.Vfs.lookup(\"" + sourcePath
903: + "\"), " + digest + "L)");
904: }
905: }
906: }
|