001 /**
002 * <copyright>
003 *
004 * Copyright (c) 2004-2008 IBM Corporation and others.
005 * All rights reserved. This program and the accompanying materials
006 * are made available under the terms of the Eclipse Public License v1.0
007 * which accompanies this distribution, and is available at
008 * http://www.eclipse.org/legal/epl-v10.html
009 *
010 * Contributors:
011 * IBM - Initial API and implementation
012 *
013 * </copyright>
014 *
015 * $Id$
016 */
017 package org.eclipse.emf.common.archive;
018
019 import java.io.File;
020 import java.io.FileInputStream;
021 import java.io.FileOutputStream;
022 import java.io.FilterInputStream;
023 import java.io.FilterOutputStream;
024 import java.io.IOException;
025 import java.io.InputStream;
026 import java.io.OutputStream;
027 import java.net.MalformedURLException;
028 import java.net.URL;
029 import java.net.URLConnection;
030 import java.util.zip.ZipEntry;
031 import java.util.zip.ZipFile;
032 import java.util.zip.ZipInputStream;
033 import java.util.zip.ZipOutputStream;
034
035 import org.eclipse.emf.common.util.URI;
036
037 /**
038 * A connection that can access an entry in an archive, and then recursively an entry in that archive, and so on.
039 * For example, it can be used just like jar: or zip:, only the archive paths can repeat, e.g.,
040 *<pre>
041 * archive:file:///c:/temp/example.zip!/org/example/nested.zip!/org/example/deeply-nested.html
042 *</pre>
043 * The general recursive pattern is
044 *<pre>
045 * archive:$nestedURL${/!$archivePath$}+
046 *</pre>
047 * So the nested URL for the example above is
048 *<pre>
049 * file:///c:/temp/example.zip
050 *</pre>
051 *
052 * <p>
053 * Since the nested URL may itself contain archive schemes,
054 * the subsequence of the archive paths that should be associated with the nested URL
055 * is determined by finding the nth archive separator, i.e., the nth !/,
056 * where n is the number of ":"s before the first "/" of the nested URL, i.e., the number of nested schemes.
057 * For example, for a more complex case where the nested URL is itself an archive-based scheme, e.g.,
058 *<pre>
059 * archive:jar:file:///c:/temp/example.zip!/org/example/nested.zip!/org/example/deeply-nested.html
060 *</pre>
061 * the nested URL is correctly parsed to skip to the second archive separator as
062 *<pre>
063 * jar:file:///c:/temp/example.zip!/org/example/nested.zip
064 *</pre>
065 * </p>
066 *
067 * <p>
068 * The logic for accessing archives can be tailored and reused independant from its usage as a URL connection.
069 * This is normally done by using the constructor {@link #ArchiveURLConnection(String)}
070 * and overriding {@link #createInputStream(String)} and {@link #createOutputStream(String)}.
071 * The behavior can be tailored by overriding {@link #emulateArchiveScheme()} and {@link #useZipFile()}.
072 * </p>
073 */
074 public class ArchiveURLConnection extends URLConnection
075 {
076 /**
077 * The cached string version of the {@link #url URL}.
078 */
079 protected String urlString;
080
081 /**
082 * Constructs a new connection for the URL.
083 * @param url the URL of this connection.
084 */
085 public ArchiveURLConnection(URL url)
086 {
087 super(url);
088 urlString = url.toString();
089 }
090
091 /**
092 * Constructs a new archive accessor.
093 * This constructor forwards a null URL to be super constructor,
094 * so an instance built with this constructor <b>cannot</b> be used as a URLConnection.
095 * The logic for accessing archives and for delegating to the nested URL can be reused in other applications,
096 * without creating an URLs.
097 * @param url the URL of the archive.
098 */
099 protected ArchiveURLConnection(String url)
100 {
101 super(null);
102 urlString = url;
103 }
104
105 /**
106 * </p>
107 * Returns whether the implementation will handle all the archive accessors directly.
108 * For example, whether
109 *<pre>
110 * archive:jar:file:///c:/temp/example.zip!/org/example/nested.zip!/org/example/deeply-nested.html
111 *</pre>
112 * will be handled as if it were specified as
113 *<pre>
114 * archive:file:///c:/temp/example.zip!/org/example/nested.zip!/org/example/deeply-nested.html
115 *</pre>
116 * Override this only if you are reusing the logic of retrieving an input stream into an archive
117 * and hence are likely to be overriding createInputStream,
118 * which is the point of delegation to the nested URL for recursive stream creation.
119 * </p>
120 * @return whether the implementation will handle all the archive accessors directly.
121 */
122 protected boolean emulateArchiveScheme()
123 {
124 return false;
125 }
126
127 /**
128 * Returns whether to handle the special case of a nested URL with file: schema using a {@link ZipFile}.
129 * This gives more efficient direct access to the root entry, e.g.,
130 *<pre>
131 * archive:file:///c:/temp/example.zip!/org/example/nested.html
132 *</pre>
133 * @return whether to handle the special case of a nested URL with file: schema using a ZipFile.
134 */
135 protected boolean useZipFile()
136 {
137 return false;
138 }
139
140 /**
141 * Record that this is connected.
142 */
143 @Override
144 public void connect() throws IOException
145 {
146 connected = true;
147 }
148
149 protected String getNestedURL() throws IOException
150 {
151 // There must be at least one archive path.
152 //
153 int archiveSeparator = urlString.indexOf("!/");
154 if (archiveSeparator < 0)
155 {
156 throw new MalformedURLException("missing archive separators " + urlString);
157 }
158
159 // There needs to be another URL protocol right after the archive protocol, and not a "/".
160 //
161 int start = urlString.indexOf(':') + 1;
162 if (start > urlString.length() || urlString.charAt(start) == '/')
163 {
164 throw
165 new IllegalArgumentException
166 ("archive protocol must be immediately followed by another URL protocol " + urlString);
167 }
168
169 // Parse to extract the archives that will be delegated to the nested URL based on the number of schemes at the start.
170 //
171 for (int i = start, end = urlString.indexOf("/") - 1; (i = urlString.indexOf(":", i)) < end; )
172 {
173 if (emulateArchiveScheme())
174 {
175 // Skip a scheme for the archive accessor to be handled directly here.
176 //
177 start = ++i;
178 }
179 else
180 {
181 // Skip an archive accessor to be handled by delegation to the scheme in nested URL.
182 //
183 archiveSeparator = urlString.indexOf("!/", archiveSeparator + 2);
184 if (archiveSeparator < 0)
185 {
186 throw new MalformedURLException("too few archive separators " + urlString);
187 }
188 ++i;
189 }
190 }
191
192 return urlString.substring(start, archiveSeparator);
193 }
194
195 /**
196 * Creates the input stream for the URL.
197 * @return the input stream for the URL.
198 */
199 @Override
200 public InputStream getInputStream() throws IOException
201 {
202 // Create the delegate URL.
203 //
204 String nestedURL = getNestedURL();
205
206 // The cutoff point to the next archive.
207 //
208 int archiveSeparator = urlString.indexOf(nestedURL) + nestedURL.length();
209 int nextArchiveSeparator = urlString.indexOf("!/", archiveSeparator + 2);
210
211 // Construct the input stream in a special efficient way for case of a file scheme.
212 //
213 InputStream inputStream;
214 ZipEntry inputZipEntry = null;
215 if (!useZipFile() || !nestedURL.startsWith("file:"))
216 {
217 // Just get the stream from the URL.
218 //
219 inputStream = createInputStream(nestedURL);
220 }
221 else
222 {
223 // The name to be used for the entry.
224 //
225 String entry =
226 URI.decode(nextArchiveSeparator < 0 ?
227 urlString.substring(archiveSeparator + 2) :
228 urlString.substring(archiveSeparator + 2, nextArchiveSeparator));
229
230 // Skip over this archive path to the next one, since we are handling this one special.
231 //
232 archiveSeparator = nextArchiveSeparator;
233 nextArchiveSeparator = urlString.indexOf("!/", archiveSeparator + 2);
234
235 // Go directly to the right entry in the zip file,
236 // get the stream,
237 // and wrap it so that closing it closes the zip file.
238 //
239 final ZipFile zipFile = new ZipFile(URI.decode(nestedURL.substring(5)));
240 inputZipEntry = zipFile.getEntry(entry);
241 InputStream zipEntryInputStream = inputZipEntry == null ? null : zipFile.getInputStream(inputZipEntry);
242 if (zipEntryInputStream == null)
243 {
244 try
245 {
246 zipFile.close();
247 }
248 catch (Throwable throwable)
249 {
250 // Ignore because we'll throw a different IO exception
251 }
252 throw new IOException("Archive entry not found " + urlString);
253 }
254 inputStream =
255 new FilterInputStream(zipEntryInputStream)
256 {
257 @Override
258 public void close() throws IOException
259 {
260 super.close();
261 zipFile.close();
262 }
263 };
264 }
265
266 // Loop over the archive paths.
267 //
268 LOOP:
269 while (archiveSeparator > 0)
270 {
271 inputZipEntry = null;
272
273 // The entry name to be matched.
274 //
275 String entry =
276 URI.decode(nextArchiveSeparator < 0 ?
277 urlString.substring(archiveSeparator + 2) :
278 urlString.substring(archiveSeparator + 2, nextArchiveSeparator));
279
280 // Wrap the input stream as a zip stream to scan it's contents for a match.
281 //
282 ZipInputStream zipInputStream = new ZipInputStream(inputStream);
283 while (zipInputStream.available() >= 0)
284 {
285 ZipEntry zipEntry = zipInputStream.getNextEntry();
286 if (zipEntry == null)
287 {
288 break;
289 }
290 else if (entry.equals(zipEntry.getName()))
291 {
292 inputZipEntry = zipEntry;
293 inputStream = zipInputStream;
294
295 // Skip to the next archive path and continue the loop.
296 //
297 archiveSeparator = nextArchiveSeparator;
298 nextArchiveSeparator = urlString.indexOf("!/", archiveSeparator + 2);
299 continue LOOP;
300 }
301 }
302
303 zipInputStream.close();
304 throw new IOException("Archive entry not found " + urlString);
305 }
306
307 return yield(inputZipEntry, inputStream);
308 }
309
310 protected InputStream yield(ZipEntry zipEntry, InputStream inputStream) throws IOException
311 {
312 return inputStream;
313 }
314
315 /**
316 * Creates an input stream for the nested URL by calling {@link URL#openStream() opening} a stream on it.
317 * @param nestedURL the nested URL for which a stream is required.
318 * @return the open stream of the nested URL.
319 */
320 protected InputStream createInputStream(String nestedURL) throws IOException
321 {
322 return new URL(nestedURL).openStream();
323 }
324
325 /**
326 * Creates the output stream for the URL.
327 * @return the output stream for the URL.
328 */
329 @Override
330 public OutputStream getOutputStream() throws IOException
331 {
332 return getOutputStream(false, -1);
333 }
334
335 public void delete() throws IOException
336 {
337 getOutputStream(true, -1).close();
338 }
339
340 public void setTimeStamp(long timeStamp) throws IOException
341 {
342 getOutputStream(false, timeStamp).close();
343 }
344
345 private OutputStream getOutputStream(boolean delete, long timeStamp) throws IOException
346 {
347 // Create the delegate URL
348 //
349 final String nestedURL = getNestedURL();
350
351 // Create a temporary file where the existing contents of the archive can be written
352 // before the new contents are added.
353 //
354 final File tempFile = File.createTempFile("Archive", "zip");
355 tempFile.deleteOnExit();
356
357 // Record the input and output streams for closing in case of failure so that handles are not left open.
358 //
359 InputStream sourceInputStream = null;
360 OutputStream tempOutputStream = null;
361 try
362 {
363 // Create the output stream to the temporary file and the input stream for the delegate URL.
364 //
365 tempOutputStream = new FileOutputStream(tempFile);
366 try
367 {
368 sourceInputStream = createInputStream(nestedURL);
369 }
370 catch (IOException exception)
371 {
372 // Continue processing if the file doesn't exist so that we try create a new empty one.
373 }
374
375 // Record them as generic streams to record state during the loop that emulates recursion.
376 //
377 OutputStream outputStream = tempOutputStream;
378 InputStream inputStream = sourceInputStream;
379
380 // The cutoff point to the next archive.
381 //
382 int archiveSeparator = urlString.indexOf(nestedURL) + nestedURL.length();
383 int nextArchiveSeparator = urlString.indexOf("!/", archiveSeparator + 2);
384
385 // The most deeply nested output stream that will be returned wrapped as the result.
386 //
387 ZipOutputStream zipOutputStream;
388
389 // A buffer for transferring archive contents.
390 //
391 final byte [] bytes = new byte [4096];
392
393 // We expect there to be at least one archive path.
394 //
395 ZipEntry outputZipEntry;
396 boolean found = false;
397 for (;;)
398 {
399 // The name that will be used as the archive entry.
400 //
401 String entry =
402 URI.decode(nextArchiveSeparator < 0 ?
403 urlString.substring(archiveSeparator + 2) :
404 urlString.substring(archiveSeparator + 2, nextArchiveSeparator));
405
406 // Wrap the current result as a zip stream, and record it for loop-based recursion.
407 //
408 zipOutputStream = null;
409
410 // Wrap the current input as a zip stream, and record it for loop-based recursion.
411 //
412 ZipInputStream zipInputStream = inputStream == null ? null : new ZipInputStream(inputStream);
413 inputStream = zipInputStream;
414
415 // Loop over the entries in the zip stream.
416 //
417 while (zipInputStream != null && zipInputStream.available() >= 0)
418 {
419 // If this entry isn't the end marker
420 // and isn't the matching one that we are replacing...
421 //
422 ZipEntry zipEntry = zipInputStream.getNextEntry();
423 if (zipEntry == null)
424 {
425 break;
426 }
427 else
428 {
429 boolean match = entry.equals(zipEntry.getName());
430 if (!found)
431 {
432 found = match && nextArchiveSeparator < 0;
433 }
434 if (timeStamp != -1 || !match)
435 {
436 if (zipOutputStream == null)
437 {
438 zipOutputStream = new ZipOutputStream(outputStream);
439 outputStream = zipOutputStream;
440 }
441 // Transfer the entry and its contents.
442 //
443 if (timeStamp != -1 && match && nextArchiveSeparator < 0)
444 {
445 zipEntry.setTime(timeStamp);
446 }
447 zipOutputStream.putNextEntry(zipEntry);
448 for (int size; (size = zipInputStream.read(bytes, 0, bytes.length)) > -1; )
449 {
450 zipOutputStream.write(bytes, 0, size);
451 }
452 }
453 }
454 }
455
456 // Find the next archive path and continue "recursively" if there is one.
457 //
458 archiveSeparator = nextArchiveSeparator;
459 nextArchiveSeparator = urlString.indexOf("!/", archiveSeparator + 2);
460
461 if ((delete || timeStamp != -1) && archiveSeparator < 0)
462 {
463 if (!found)
464 {
465 throw new IOException("Archive entry not found " + urlString);
466 }
467 // Create no entry since we are deleting and return immediately.
468 //
469 outputZipEntry = null;
470 break;
471 }
472 else
473 {
474 // Create a new or replaced entry and continue processing the remaining archives.
475 //
476 outputZipEntry = new ZipEntry(entry);
477 if (zipOutputStream == null)
478 {
479 zipOutputStream = new ZipOutputStream(outputStream);
480 outputStream = zipOutputStream;
481 }
482 zipOutputStream.putNextEntry(outputZipEntry);
483 if (archiveSeparator > 0)
484 {
485 continue;
486 }
487 else
488 {
489 break;
490 }
491 }
492 }
493
494 // Ensure that it won't be closed in the finally block.
495 //
496 tempOutputStream = null;
497
498 // Wrap the deepest result so that on close, the results are finally transferred.
499 //
500 final boolean deleteRequired = sourceInputStream != null;
501 FilterOutputStream result =
502 new FilterOutputStream(zipOutputStream == null ? outputStream : zipOutputStream)
503 {
504 protected boolean isClosed;
505
506 @Override
507 public void close() throws IOException
508 {
509 // Make sure we close only once.
510 //
511 if (!isClosed)
512 {
513 isClosed = true;
514
515 // Close for real so that the temporary file is ready to be read.
516 //
517 super.close();
518
519 boolean useRenameTo = nestedURL.startsWith("file:");
520
521 // If the delegate URI can be handled as a file,
522 // we'll hope that renaming it will be really efficient.
523 //
524 if (useRenameTo)
525 {
526 File targetFile = new File(URI.decode(nestedURL.substring(5)));
527 if (deleteRequired && !targetFile.delete())
528 {
529 throw new IOException("cannot delete " + targetFile.getPath());
530 }
531 else if (!tempFile.renameTo(targetFile))
532 {
533 useRenameTo = false;
534 }
535 }
536 if (!useRenameTo)
537 {
538 // Try to transfer it by reading the contents of the temporary file
539 // and writing them to the output stream of the delegate.
540 //
541 InputStream inputStream = null;
542 OutputStream outputStream = null;
543 try
544 {
545 inputStream = new FileInputStream(tempFile);
546 outputStream = createOutputStream(nestedURL);
547 for (int size; (size = inputStream.read(bytes, 0, bytes.length)) > -1; )
548 {
549 outputStream.write(bytes, 0, size);
550 }
551 }
552 finally
553 {
554 // Make sure they are closed no matter what bad thing happens.
555 //
556 if (inputStream != null)
557 {
558 inputStream.close();
559 }
560 if (outputStream != null)
561 {
562 outputStream.close();
563 }
564 }
565 }
566 // Delete the temporary file early if possible
567 //
568 tempFile.delete();
569 }
570 }
571 };
572 return outputZipEntry == null ? result : yield(outputZipEntry, result);
573 }
574 finally
575 {
576 // Close in case of failure to complete.
577 //
578 if (tempOutputStream != null)
579 {
580 tempOutputStream.close();
581 }
582
583 // Close if we created this.
584 //
585 if (sourceInputStream != null)
586 {
587 sourceInputStream.close();
588 }
589 }
590 }
591
592 protected OutputStream yield(ZipEntry zipEntry, OutputStream outputStream) throws IOException
593 {
594 return outputStream;
595 }
596
597
598 /**
599 * Creates an output stream for the nested URL by calling {@link URL#openConnection() opening} a stream on it.
600 * @param nestedURL the nested URL for which a stream is required.
601 * @return the open stream of the nested URL.
602 */
603 protected OutputStream createOutputStream(String nestedURL) throws IOException
604 {
605 URL url = new URL(nestedURL);
606 URLConnection urlConnection = url.openConnection();
607 urlConnection.setDoOutput(true);
608 return urlConnection.getOutputStream();
609 }
610 }