001: /*****************************************************************************
002: * Java Plug-in Framework (JPF)
003: * Copyright (C) 2004-2006 Dmitry Olshansky
004: *
005: * This library is free software; you can redistribute it and/or
006: * modify it under the terms of the GNU Lesser General Public
007: * License as published by the Free Software Foundation; either
008: * version 2.1 of the License, or (at your option) any later version.
009: *
010: * This library is distributed in the hope that it will be useful,
011: * but WITHOUT ANY WARRANTY; without even the implied warranty of
012: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
013: * Lesser General Public License for more details.
014: *
015: * You should have received a copy of the GNU Lesser General Public
016: * License along with this library; if not, write to the Free Software
017: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
018: *****************************************************************************/package org.java.plugin.tools;
019:
020: import java.io.BufferedInputStream;
021: import java.io.BufferedOutputStream;
022: import java.io.File;
023: import java.io.FileInputStream;
024: import java.io.FileOutputStream;
025: import java.io.IOException;
026: import java.io.ObjectInputStream;
027: import java.io.ObjectOutputStream;
028: import java.io.OutputStream;
029: import java.io.Serializable;
030: import java.net.URL;
031: import java.util.HashMap;
032: import java.util.HashSet;
033: import java.util.Map;
034: import java.util.Set;
035: import java.util.zip.ZipEntry;
036: import java.util.zip.ZipInputStream;
037: import java.util.zip.ZipOutputStream;
038: import org.java.plugin.ObjectFactory;
039: import org.java.plugin.PathResolver;
040: import org.java.plugin.registry.Identity;
041: import org.java.plugin.registry.ManifestProcessingException;
042: import org.java.plugin.registry.PluginDescriptor;
043: import org.java.plugin.registry.PluginFragment;
044: import org.java.plugin.registry.PluginRegistry;
045: import org.java.plugin.registry.Version;
046: import org.java.plugin.util.IoUtil;
047:
048: /**
049: * Plug-ins archive support class.
050: * @version $Id$
051: */
052: public final class PluginArchiver {
053: private static final String DESCRIPTOR_ENTRY_NAME = "JPF-DESCRIPTOR"; //$NON-NLS-1$
054:
055: /**
056: * Packs given plug-in into single ZIP file. Resulting file may be used to
057: * run plug-ins from.
058: * @param descr plug-in descriptor
059: * @param pathResolver path resolver instance
060: * @param destFile target file
061: * @throws IOException if an I/O error has occurred
062: */
063: public static void pack(final PluginDescriptor descr,
064: final PathResolver pathResolver, final File destFile)
065: throws IOException {
066: pack(pathResolver.resolvePath(descr, "/"), //$NON-NLS-1$
067: "JPF plug-in " + descr.getId() //$NON-NLS-1$
068: + " of version " + descr.getVersion(), destFile); //$NON-NLS-1$
069: }
070:
071: /**
072: * Packs given plug-in fragment into single ZIP file. Resulting file may be
073: * used to run plug-ins from.
074: * @param fragment plug-in fragment descriptor
075: * @param pathResolver path resolver instance
076: * @param destFile target file
077: * @throws IOException if an I/O error has occurred
078: */
079: public static void pack(final PluginFragment fragment,
080: final PathResolver pathResolver, final File destFile)
081: throws IOException {
082: pack(
083: pathResolver.resolvePath(fragment, "/"), //$NON-NLS-1$
084: "JPF plug-in fragment " + fragment.getId() //$NON-NLS-1$
085: + " of version " + fragment.getVersion(), destFile); //$NON-NLS-1$
086: }
087:
088: private static void pack(final URL url, final String comment,
089: final File destFile) throws IOException {
090: ZipOutputStream zipStrm = new ZipOutputStream(
091: new BufferedOutputStream(new FileOutputStream(destFile,
092: false)));
093: try {
094: zipStrm.setComment(comment);
095: File file = IoUtil.url2file(url);
096: if (file == null) {
097: throw new IOException("resolved URL " + url //$NON-NLS-1$
098: + " is not local file system location pointer"); //$NON-NLS-1$
099: }
100: File[] files = file.listFiles();
101: for (int i = 0; i < files.length; i++) {
102: packEntry(zipStrm, null, files[i]);
103: }
104: } finally {
105: zipStrm.close();
106: }
107: }
108:
109: /**
110: * Packs all plug-ins from given registry as one archive file.
111: * @param registry plug-ins registry
112: * @param pathResolver path resolver (only local file URLs are supported)
113: * @param destFile target archive file (will be overridden if any exists)
114: * @return set of UID's of all packed plug-ins
115: * @throws IOException if an I/O error has occurred
116: */
117: public static Set<String> pack(final PluginRegistry registry,
118: final PathResolver pathResolver, final File destFile)
119: throws IOException {
120: return pack(registry, pathResolver, destFile, new Filter() {
121: public boolean accept(final String id,
122: final Version version, final boolean isFragment) {
123: return true;
124: }
125: });
126: }
127:
128: /**
129: * Packs plug-ins from given registry as one archive file according to
130: * given filter.
131: * @param registry plug-ins registry
132: * @param pathResolver path resolver (only local file URLs are supported)
133: * @param destFile target archive file (will be overridden if any exists)
134: * @param filter filter to be used when packing plug-ins
135: * @return set of UID's of all packed plug-ins
136: * @throws IOException if an I/O error has occurred
137: */
138: public static Set<String> pack(final PluginRegistry registry,
139: final PathResolver pathResolver, final File destFile,
140: final Filter filter) throws IOException {
141: Set<String> result;
142: ZipOutputStream zipStrm = new ZipOutputStream(
143: new BufferedOutputStream(new FileOutputStream(destFile,
144: false)));
145: try {
146: zipStrm.setComment("JPF plug-ins archive"); //$NON-NLS-1$
147: ZipEntry entry = new ZipEntry(DESCRIPTOR_ENTRY_NAME);
148: entry.setComment("JPF plug-ins archive descriptor"); //$NON-NLS-1$
149: zipStrm.putNextEntry(entry);
150: result = writeDescripor(registry, filter,
151: new ObjectOutputStream(zipStrm));
152: zipStrm.closeEntry();
153: for (PluginDescriptor descr : registry
154: .getPluginDescriptors()) {
155: if (!result.contains(descr.getUniqueId())) {
156: continue;
157: }
158: URL url = pathResolver.resolvePath(descr, "/"); //$NON-NLS-1$
159: File file = IoUtil.url2file(url);
160: if (file == null) {
161: throw new IOException(
162: "resolved URL " + url //$NON-NLS-1$
163: + " is not local file system location pointer"); //$NON-NLS-1$
164: }
165: entry = new ZipEntry(descr.getUniqueId() + "/"); //$NON-NLS-1$
166: entry.setComment("Content for JPF plug-in " //$NON-NLS-1$
167: + descr.getId()
168: + " version " + descr.getVersion()); //$NON-NLS-1$
169: entry.setTime(file.lastModified());
170: zipStrm.putNextEntry(entry);
171: File[] files = file.listFiles();
172: for (int i = 0; i < files.length; i++) {
173: packEntry(zipStrm, entry, files[i]);
174: }
175: }
176: for (PluginFragment fragment : registry
177: .getPluginFragments()) {
178: if (!result.contains(fragment.getUniqueId())) {
179: continue;
180: }
181: URL url = pathResolver.resolvePath(fragment, "/"); //$NON-NLS-1$
182: File file = IoUtil.url2file(url);
183: if (file == null) {
184: throw new IOException(
185: "resolved URL " + url //$NON-NLS-1$
186: + " is not local file system location pointer"); //$NON-NLS-1$
187: }
188: entry = new ZipEntry(fragment.getUniqueId() + "/"); //$NON-NLS-1$
189: entry.setComment("Content for JPF plug-in fragment " //$NON-NLS-1$
190: + fragment.getId() + " version " //$NON-NLS-1$
191: + fragment.getVersion());
192: entry.setTime(file.lastModified());
193: zipStrm.putNextEntry(entry);
194: File[] files = file.listFiles();
195: for (int i = 0; i < files.length; i++) {
196: packEntry(zipStrm, entry, files[i]);
197: }
198: }
199: } finally {
200: zipStrm.close();
201: }
202: return result;
203: }
204:
205: private static void packEntry(final ZipOutputStream zipStrm,
206: final ZipEntry parentEntry, final File file)
207: throws IOException {
208: String parentEntryName = (parentEntry == null) ? "" //$NON-NLS-1$
209: : parentEntry.getName();
210: if (file.isFile()) {
211: ZipEntry entry = new ZipEntry(parentEntryName
212: + file.getName());
213: entry.setTime(file.lastModified());
214: zipStrm.putNextEntry(entry);
215: BufferedInputStream fileStrm = new BufferedInputStream(
216: new FileInputStream(file));
217: try {
218: IoUtil.copyStream(fileStrm, zipStrm, 1024);
219: } finally {
220: fileStrm.close();
221: }
222: return;
223: }
224: ZipEntry entry = new ZipEntry(parentEntryName + file.getName()
225: + "/"); //$NON-NLS-1$
226: entry.setTime(file.lastModified());
227: zipStrm.putNextEntry(entry);
228: File[] files = file.listFiles();
229: for (int i = 0; i < files.length; i++) {
230: packEntry(zipStrm, entry, files[i]);
231: }
232: }
233:
234: /**
235: * Extracts plug-ins from the given archive file.
236: * @param archiveFile plug-in archive file
237: * @param registry plug-in registry where to register manifests for
238: * unpacked plug-ins
239: * @param destFolder target folder
240: * @return set of UID's of all un-packed (and registered) plug-ins
241: * @throws IOException if an I/O error has occurred
242: * @throws ClassNotFoundException if descriptor can't be read
243: * @throws ManifestProcessingException if manifest can't be registered
244: * (optional behavior)
245: *
246: * @see #unpack(URL, PluginRegistry, File, PluginArchiver.Filter)
247: */
248: public static Set<String> unpack(final URL archiveFile,
249: final PluginRegistry registry, final File destFolder)
250: throws ManifestProcessingException, IOException,
251: ClassNotFoundException {
252: return unpack(archiveFile, registry, destFolder, new Filter() {
253: public boolean accept(final String id,
254: final Version version, final boolean isFragment) {
255: return true;
256: }
257: });
258: }
259:
260: /**
261: * Extracts plug-ins from the given archive file.
262: * <br>
263: * <b>Note:</b>
264: * <br>
265: * In the current implementation all plug-in manifests are extracted to
266: * temporary local storage and deleted immediately after their registration
267: * with plug-in registry. So manifest URL's are actually point to "fake"
268: * locations.
269: * @param archiveFile plug-in archive file
270: * @param registry plug-in registry where to register manifests for
271: * unpacked plug-ins
272: * @param destFolder target folder
273: * @param filter filter to be used when un-packing plug-ins
274: * @return set of UID's of all un-packed (and registered) plug-ins
275: * @throws ClassNotFoundException if plug-ins archive descriptor can't be
276: * de-serialized
277: * @throws ManifestProcessingException if plug-in manifests can't be
278: * registered
279: * @throws IOException if archive damaged or I/O error has occurred
280: */
281: public static Set<String> unpack(final URL archiveFile,
282: final PluginRegistry registry, final File destFolder,
283: final Filter filter) throws IOException,
284: ManifestProcessingException, ClassNotFoundException {
285: Set<String> result;
286: int count = 0;
287: ZipInputStream zipStrm = new ZipInputStream(
288: new BufferedInputStream(archiveFile.openStream()));
289: try {
290: ZipEntry entry = zipStrm.getNextEntry();
291: //NB: we are expecting that descriptor is in the first ZIP entry
292: if (entry == null) {
293: throw new IOException(
294: "invalid plug-ins archive, no entries found"); //$NON-NLS-1$
295: }
296: if (!DESCRIPTOR_ENTRY_NAME.equals(entry.getName())) {
297: throw new IOException(
298: "invalid plug-ins archive " + archiveFile //$NON-NLS-1$
299: + ", entry " + DESCRIPTOR_ENTRY_NAME //$NON-NLS-1$
300: + " not found as first ZIP entry in the archive file"); //$NON-NLS-1$
301: }
302: ObjectInputStream strm = new ObjectInputStream(zipStrm);
303: result = readDescriptor(strm, registry, destFolder, filter);
304: entry = zipStrm.getNextEntry();
305: while (entry != null) {
306: String name = entry.getName();
307: if (name.endsWith("/") //$NON-NLS-1$
308: && (name.lastIndexOf('/', name.length() - 2) == -1)) {
309: String uid = name.substring(0, name.length() - 1);
310: if (!result.contains(uid)) {
311: entry = zipStrm.getNextEntry();
312: continue;
313: }
314: count++;
315: } else {
316: int p = name.indexOf('/');
317: if ((p == -1) || (p == 0)
318: || !result.contains(name.substring(0, p))) {
319: entry = zipStrm.getNextEntry();
320: continue;
321: }
322: }
323: unpackEntry(zipStrm, entry, destFolder);
324: entry = zipStrm.getNextEntry();
325: }
326: } finally {
327: zipStrm.close();
328: }
329: if (result.size() != count) {
330: throw new IOException(
331: "invalid plug-ins number (" + count //$NON-NLS-1$
332: + ") found in the archive, expected number according to " //$NON-NLS-1$
333: + "the archive descriptor is " + result.size()); //$NON-NLS-1$
334: }
335: return result;
336: }
337:
338: /**
339: * Extracts all plug-ins from the given archive file.
340: * <br>
341: * <b>Note:</b>
342: * <br>
343: * {@link ObjectFactory#createRegistry() Standard plug-in registry}
344: * implementation will be used internally to read plug-in manifests.
345: * @param archiveFile plug-in archive file
346: * @param destFolder target folder
347: * @return set of UID's of all un-packed plug-ins
348: * @throws IOException if an I/O error has occurred
349: * @throws ClassNotFoundException if descriptor can't be read
350: * @throws ManifestProcessingException if manifest can't be registered
351: * (optional behavior)
352: *
353: * @see ObjectFactory#createRegistry()
354: */
355: public static Set<String> unpack(final URL archiveFile,
356: final File destFolder) throws ManifestProcessingException,
357: IOException, ClassNotFoundException {
358: return unpack(archiveFile, ObjectFactory.newInstance()
359: .createRegistry(), destFolder);
360: }
361:
362: /**
363: * Extracts plug-ins from the given archive file according to given filter.
364: * <br>
365: * <b>Note:</b>
366: * <br>
367: * {@link ObjectFactory#createRegistry() Standard plug-in registry}
368: * implementation will be used internally to read plug-in manifests.
369: * @param archiveFile plug-in archive file
370: * @param destFolder target folder
371: * @param filter filter to be used when un-packing plug-ins
372: * @return set of UID's of all un-packed plug-ins
373: * @throws IOException if an I/O error has occurred
374: * @throws ClassNotFoundException if descriptor can't be read
375: * @throws ManifestProcessingException if manifest can't be registered
376: * (optional behavior)
377: */
378: public static Set<String> unpack(final URL archiveFile,
379: final File destFolder, final Filter filter)
380: throws ManifestProcessingException, IOException,
381: ClassNotFoundException {
382: return unpack(archiveFile, ObjectFactory.newInstance()
383: .createRegistry(), destFolder, filter);
384: }
385:
386: private static void unpackEntry(final ZipInputStream zipStrm,
387: final ZipEntry entry, final File destFolder)
388: throws IOException {
389: String name = entry.getName();
390: if (name.endsWith("/")) { //$NON-NLS-1$
391: File folder = new File(destFolder.getCanonicalPath()
392: + "/" + name); //$NON-NLS-1$
393: if (!folder.exists() && !folder.mkdirs()) {
394: throw new IOException("can't create folder " + folder); //$NON-NLS-1$
395: }
396: folder.setLastModified(entry.getTime());
397: return;
398: }
399: File file = new File(destFolder.getCanonicalPath() + "/" + name); //$NON-NLS-1$
400: File folder = file.getParentFile();
401: if (!folder.exists() && !folder.mkdirs()) {
402: throw new IOException("can't create folder " + folder); //$NON-NLS-1$
403: }
404: OutputStream strm = new BufferedOutputStream(
405: new FileOutputStream(file, false));
406: try {
407: IoUtil.copyStream(zipStrm, strm, 1024);
408: } finally {
409: strm.close();
410: }
411: file.setLastModified(entry.getTime());
412: }
413:
414: /**
415: * Reads meta-information from plug-ins archive file and registers found
416: * plug-in manifest data with given registry for future analysis.
417: * @param archiveFile plug-in archive file
418: * @param registry plug-in registry where to register discovered manifests
419: * for archived plug-ins
420: * @return set of UID's of all registered plug-ins
421: * @throws IOException if an I/O error has occurred
422: * @throws ClassNotFoundException if descriptor can't be read
423: * @throws ManifestProcessingException if manifest can't be registered
424: * (optional behavior)
425: *
426: * @see #readDescriptor(URL, PluginRegistry, PluginArchiver.Filter)
427: */
428: public static Set<String> readDescriptor(final URL archiveFile,
429: final PluginRegistry registry) throws IOException,
430: ClassNotFoundException, ManifestProcessingException {
431: return readDescriptor(archiveFile, registry, new Filter() {
432: public boolean accept(final String id,
433: final Version version, final boolean isFragment) {
434: return true;
435: }
436: });
437: }
438:
439: /**
440: * Reads meta-information from plug-ins archive file and registers found
441: * plug-in manifest data with given registry for future analysis.
442: * <br>
443: * <b>Note:</b>
444: * <br>
445: * In the current implementation all plug-in manifests are extracted to
446: * temporary local storage and deleted immediately after their registration
447: * with plug-in registry. So manifest URL's are actually point to "fake"
448: * locations and main purpose of this method is to allow you to analyze
449: * plug-ins archive without needing to download and unpack it.
450: * @param archiveFile plug-in archive file
451: * @param registry plug-in registry where to register discovered manifests
452: * for archived plug-ins
453: * @param filter filter to be used when un-packing plug-ins
454: * @return set of UID's of all registered plug-ins
455: * @throws IOException if an I/O error has occurred
456: * @throws ClassNotFoundException if descriptor can't be read
457: * @throws ManifestProcessingException if manifest can't be registered
458: * (optional behavior)
459: */
460: public static Set<String> readDescriptor(final URL archiveFile,
461: final PluginRegistry registry, final Filter filter)
462: throws IOException, ClassNotFoundException,
463: ManifestProcessingException {
464: ZipInputStream zipStrm = new ZipInputStream(
465: new BufferedInputStream(archiveFile.openStream()));
466: try {
467: ZipEntry entry = zipStrm.getNextEntry();
468: //NB: we are expecting that descriptor is in the first ZIP entry
469: if (entry == null) {
470: throw new IOException(
471: "invalid plug-ins archive, no entries found"); //$NON-NLS-1$
472: }
473: if (!DESCRIPTOR_ENTRY_NAME.equals(entry.getName())) {
474: throw new IOException(
475: "invalid plug-ins archive " + archiveFile //$NON-NLS-1$
476: + ", entry " + DESCRIPTOR_ENTRY_NAME //$NON-NLS-1$
477: + " not found as first ZIP entry in the archive file"); //$NON-NLS-1$
478: }
479: ObjectInputStream strm = new ObjectInputStream(zipStrm);
480: return readDescriptor(strm, registry, Util.getTempFolder(),
481: filter);
482: } finally {
483: zipStrm.close();
484: }
485: }
486:
487: private static Set<String> writeDescripor(
488: final PluginRegistry registry, final Filter filter,
489: final ObjectOutputStream strm) throws IOException {
490: final Map<String, ArchiveDescriptorEntry> result = new HashMap<String, ArchiveDescriptorEntry>();
491: for (PluginDescriptor descr : registry.getPluginDescriptors()) {
492: if (!filter
493: .accept(descr.getId(), descr.getVersion(), false)) {
494: continue;
495: }
496: result.put(descr.getUniqueId(), new ArchiveDescriptorEntry(
497: descr.getId(), descr.getVersion(), false, Util
498: .readUrlContent(descr.getLocation())));
499: }
500: for (PluginFragment fragment : registry.getPluginFragments()) {
501: if (!filter.accept(fragment.getId(), fragment.getVersion(),
502: true)) {
503: continue;
504: }
505: result.put(fragment.getUniqueId(),
506: new ArchiveDescriptorEntry(fragment.getId(),
507: fragment.getVersion(), true, Util
508: .readUrlContent(fragment
509: .getLocation())));
510: }
511: strm.writeObject(result.values().toArray(
512: new ArchiveDescriptorEntry[result.size()]));
513: return result.keySet();
514: }
515:
516: private static Set<String> readDescriptor(
517: final ObjectInputStream strm,
518: final PluginRegistry registry, final File tempFolder,
519: final Filter filter) throws IOException,
520: ClassNotFoundException, ManifestProcessingException {
521: ArchiveDescriptorEntry[] data = (ArchiveDescriptorEntry[]) strm
522: .readObject();
523: // For simplicity we'll store manifests to a temporary files rather than
524: // create special URL's and provide special URL handler for them.
525: // More powerful approach will be possibly implemented in the future.
526: Set<URL> urls = new HashSet<URL>();
527: Set<File> files = new HashSet<File>();
528: for (int i = 0; i < data.length; i++) {
529: if (!filter.accept(data[i].getId(), data[i].getVersion(),
530: data[i].isFragment())) {
531: continue;
532: }
533: File file = File.createTempFile(
534: "manifest.", null, tempFolder); //$NON-NLS-1$
535: file.deleteOnExit();
536: OutputStream fileStrm = new BufferedOutputStream(
537: new FileOutputStream(file, false));
538: try {
539: fileStrm.write(data[i].getData());
540: } finally {
541: fileStrm.close();
542: }
543: files.add(file);
544: urls.add(IoUtil.file2url(file));
545: }
546: Set<String> result = new HashSet<String>();
547: try {
548: for (Identity obj : registry.register(
549: urls.toArray(new URL[urls.size()])).values()) {
550: if (obj instanceof PluginDescriptor) {
551: result.add(((PluginDescriptor) obj).getUniqueId());
552: } else if (obj instanceof PluginFragment) {
553: result.add(((PluginFragment) obj).getUniqueId());
554: } else {
555: //NB: ignore all other elements
556: }
557: }
558: } finally {
559: for (File file : files) {
560: file.delete();
561: }
562: }
563: return result;
564: }
565:
566: private PluginArchiver() {
567: // no-op
568: }
569:
570: /**
571: * Callback interface to filter plug-ins being processed.
572: * @version $Id$
573: */
574: public static interface Filter {
575: /**
576: * @param id plug-in or plug-in fragment identifier
577: * @param version plug-in or plug-in fragment version
578: * @param isFragment <code>true</code> if given identity data
579: * corresponds to plug-in fragment
580: * @return <code>true</code> if plug-in or plug-in fragment with given
581: * identity should be taken into account
582: */
583: boolean accept(String id, Version version, boolean isFragment);
584: }
585:
586: private static class ArchiveDescriptorEntry implements Serializable {
587: private static final long serialVersionUID = 8749937247555974932L;
588:
589: private final String id;
590: private final Version version;
591: private final boolean isFragment;
592: private final byte[] data;
593:
594: protected ArchiveDescriptorEntry(final String anId,
595: final Version aVersion, final boolean fragment,
596: final byte[] aData) {
597: id = anId;
598: version = aVersion;
599: isFragment = fragment;
600: data = aData;
601: }
602:
603: protected String getId() {
604: return id;
605: }
606:
607: protected Version getVersion() {
608: return version;
609: }
610:
611: protected boolean isFragment() {
612: return isFragment;
613: }
614:
615: protected byte[] getData() {
616: return data;
617: }
618: }
619: }
|