ZipJarCrawler.java
/* Copyright 2002-2017 CS Systèmes d'Information
* Licensed to CS Systèmes d'Information (CS) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* CS licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.orekit.data;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.ParseException;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.hipparchus.exception.DummyLocalizable;
import org.hipparchus.exception.LocalizedCoreFormats;
import org.orekit.errors.OrekitException;
/** Helper class for loading data files from a zip/jar archive.
* <p>
* This class browses all entries in a zip/jar archive in filesystem or in classpath.
* </p>
* <p>
* The organization of entries within the archive is unspecified. All entries are
* checked in turn. If several entries of the archive are supported by the data
* loader, all of them will be loaded.
* </p>
* <p>
* Gzip-compressed files are supported.
* </p>
* <p>
* Zip archives entries are supported recursively.
* </p>
* <p>
* This is a simple application of the <code>visitor</code> design pattern for
* zip entries browsing.
* </p>
* @see DataProvidersManager
* @author Luc Maisonobe
*/
public class ZipJarCrawler implements DataProvider {
/** Zip archive on the filesystem. */
private final File file;
/** Zip archive in the classpath. */
private final String resource;
/** Class loader to use. */
private final ClassLoader classLoader;
/** Zip archive on network. */
private final URL url;
/** Prefix name of the zip. */
private final String name;
/** Build a zip crawler for an archive file on filesystem.
* @param file zip file to browse
*/
public ZipJarCrawler(final File file) {
this.file = file;
this.resource = null;
this.classLoader = null;
this.url = null;
this.name = file.getAbsolutePath();
}
/** Build a zip crawler for an archive file in classpath.
* <p>
* Calling this constructor has the same effect as calling
* {@link #ZipJarCrawler(ClassLoader, String)} with
* {@code ZipJarCrawler.class.getClassLoader()} as first
* argument.
* </p>
* @param resource name of the zip file to browse
* @exception OrekitException if resource name is malformed
*/
public ZipJarCrawler(final String resource) throws OrekitException {
this(ZipJarCrawler.class.getClassLoader(), resource);
}
/** Build a zip crawler for an archive file in classpath.
* @param classLoader class loader to use to retrieve the resources
* @param resource name of the zip file to browse
* @exception OrekitException if resource name is malformed
*/
public ZipJarCrawler(final ClassLoader classLoader, final String resource)
throws OrekitException {
try {
this.file = null;
this.resource = resource;
this.classLoader = classLoader;
this.url = null;
this.name = classLoader.getResource(resource).toURI().toString();
} catch (URISyntaxException use) {
throw new OrekitException(use, LocalizedCoreFormats.SIMPLE_MESSAGE, use.getMessage());
}
}
/** Build a zip crawler for an archive file on network.
* @param url URL of the zip file on network
* @exception OrekitException if url syntax is malformed
*/
public ZipJarCrawler(final URL url) throws OrekitException {
try {
this.file = null;
this.resource = null;
this.classLoader = null;
this.url = url;
this.name = url.toURI().toString();
} catch (URISyntaxException use) {
throw new OrekitException(use, LocalizedCoreFormats.SIMPLE_MESSAGE, use.getMessage());
}
}
/** {@inheritDoc} */
public boolean feed(final Pattern supported, final DataLoader visitor)
throws OrekitException {
try {
// open the raw data stream
Archive archive = null;
try {
if (file != null) {
archive = new Archive(new FileInputStream(file));
} else if (resource != null) {
archive = new Archive(classLoader.getResourceAsStream(resource));
} else {
archive = new Archive(url.openConnection().getInputStream());
}
return feed(name, supported, visitor, archive);
} finally {
if (archive != null) {
archive.close();
}
}
} catch (IOException ioe) {
throw new OrekitException(ioe, new DummyLocalizable(ioe.getMessage()));
} catch (ParseException pe) {
throw new OrekitException(pe, new DummyLocalizable(pe.getMessage()));
}
}
/** Feed a data file loader by browsing the entries in a zip/jar.
* @param prefix prefix to use for name
* @param supported pattern for file names supported by the visitor
* @param visitor data file visitor to use
* @param archive archive to read
* @exception OrekitException if some data is missing, duplicated
* or can't be read
* @return true if something has been loaded
* @exception IOException if data cannot be read
* @exception ParseException if data cannot be read
*/
private boolean feed(final String prefix, final Pattern supported,
final DataLoader visitor, final Archive archive)
throws OrekitException, IOException, ParseException {
OrekitException delayedException = null;
boolean loaded = false;
// loop over all entries
for (final Archive.EntryStream entry : archive) {
try {
if (visitor.stillAcceptsData() && !entry.isDirectory()) {
final String fullName = prefix + "!" + entry.getName();
if (ZIP_ARCHIVE_PATTERN.matcher(entry.getName()).matches()) {
// recurse inside the archive entry
loaded = feed(fullName, supported, visitor, new Archive(entry)) || loaded;
} else {
// remove leading directories
String entryName = entry.getName();
final int lastSlash = entryName.lastIndexOf('/');
if (lastSlash >= 0) {
entryName = entryName.substring(lastSlash + 1);
}
// remove suffix from gzip entries
final Matcher gzipMatcher = GZIP_FILE_PATTERN.matcher(entryName);
final String baseName = gzipMatcher.matches() ? gzipMatcher.group(1) : entryName;
if (supported.matcher(baseName).matches()) {
// visit the current entry
final InputStream stream =
gzipMatcher.matches() ? new GZIPInputStream(entry) : entry;
visitor.loadData(stream, fullName);
stream.close();
loaded = true;
}
}
}
} catch (OrekitException oe) {
delayedException = oe;
}
entry.close();
}
if (!loaded && delayedException != null) {
throw delayedException;
}
return loaded;
}
/** Local class wrapping a zip archive. */
private static final class Archive implements Closeable, Iterable<Archive.EntryStream> {
/** Zip stream. */
private final ZipInputStream zip;
/** Next entry. */
private EntryStream next;
/** Simple constructor.
* @param rawStream raw stream
* @exception IOException if first entry cannot be retrieved
*/
Archive(final InputStream rawStream) throws IOException {
zip = new ZipInputStream(rawStream);
goToNext();
}
/** Go to next entry.
* @exception IOException if next entry cannot be retrieved
*/
private void goToNext() throws IOException {
final ZipEntry ze = zip.getNextEntry();
if (ze == null) {
next = null;
} else {
next = new EntryStream(ze.getName(), ze.isDirectory());
}
}
/** {@inheritDoc} */
@Override
public Iterator<Archive.EntryStream> iterator() {
return new Iterator<EntryStream> () {
/** {@inheritDoc} */
@Override
public boolean hasNext() {
return next != null;
}
/** {@inheritDoc} */
@Override
public EntryStream next() throws NoSuchElementException {
if (next == null) {
// this should never happen
throw new NoSuchElementException();
}
return next;
}
};
}
/** {@inheritDoc} */
@Override
public void close() throws IOException {
zip.close();
}
/** Archive entry. */
public class EntryStream extends InputStream {
/** Name of the entry. */
private final String name;
/** Directory indicator. */
private boolean isDirectory;
/** Indicator for already closed stream. */
private boolean closed;
/** Simple constructor.
* @param name name of the entry
* @param isDirectory if true, the entry is a directory
*/
EntryStream(final String name, final boolean isDirectory) {
this.name = name;
this.isDirectory = isDirectory;
this.closed = false;
}
/** Get the name of the entry.
* @return name of the entry
*/
public String getName() {
return name;
}
/** Check if the entry is a directory.
* @return true if the entry is a directory
*/
public boolean isDirectory() {
return isDirectory;
}
/** {@inheritDoc} */
@Override
public int read() throws IOException {
// delegate read to global input stream
return zip.read();
}
/** {@inheritDoc} */
@Override
public void close() throws IOException {
if (!closed) {
zip.closeEntry();
goToNext();
closed = true;
}
}
}
}
}