ZipJarCrawler.java

  1. /* Copyright 2002-2025 CS GROUP
  2.  * Licensed to CS GROUP (CS) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * CS licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *   http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.orekit.data;

  18. import java.io.Closeable;
  19. import java.io.File;
  20. import java.io.FileInputStream;
  21. import java.io.IOException;
  22. import java.io.InputStream;
  23. import java.net.URISyntaxException;
  24. import java.net.URL;
  25. import java.text.ParseException;
  26. import java.util.Iterator;
  27. import java.util.NoSuchElementException;
  28. import java.util.regex.Pattern;
  29. import java.util.zip.ZipEntry;
  30. import java.util.zip.ZipInputStream;

  31. import org.hipparchus.exception.DummyLocalizable;
  32. import org.hipparchus.exception.LocalizedCoreFormats;
  33. import org.orekit.errors.OrekitException;


  34. /** Helper class for loading data files from a zip/jar archive.
  35.  * <p>
  36.  * This class browses all entries in a zip/jar archive in filesystem or in classpath.
  37.  * </p>
  38.  * <p>
  39.  * The organization of entries within the archive is unspecified. All entries are
  40.  * checked in turn. If several entries of the archive are supported by the data
  41.  * loader, all of them will be loaded.
  42.  * </p>
  43.  * <p>
  44.  * All {@link FiltersManager#addFilter(DataFilter) registered}
  45.  * {@link DataFilter filters} are applied.
  46.  * </p>
  47.  * <p>
  48.  * Zip archives entries are supported recursively.
  49.  * </p>
  50.  * <p>
  51.  * This is a simple application of the <code>visitor</code> design pattern for
  52.  * zip entries browsing.
  53.  * </p>
  54.  * @see DataProvidersManager
  55.  * @author Luc Maisonobe
  56.  */
  57. public class ZipJarCrawler implements DataProvider {

  58.     /** Zip archive on the filesystem. */
  59.     private final File file;

  60.     /** Zip archive in the classpath. */
  61.     private final String resource;

  62.     /** Class loader to use. */
  63.     private final ClassLoader classLoader;

  64.     /** Zip archive on network. */
  65.     private final URL url;

  66.     /** Prefix name of the zip. */
  67.     private final String name;

  68.     /** Build a zip crawler for an archive file on filesystem.
  69.      * @param file zip file to browse
  70.      */
  71.     public ZipJarCrawler(final File file) {
  72.         this.file        = file;
  73.         this.resource    = null;
  74.         this.classLoader = null;
  75.         this.url         = null;
  76.         this.name        = file.getAbsolutePath();
  77.     }

  78.     /** Build a zip crawler for an archive file in classpath.
  79.      * <p>
  80.      * Calling this constructor has the same effect as calling
  81.      * {@link #ZipJarCrawler(ClassLoader, String)} with
  82.      * {@code ZipJarCrawler.class.getClassLoader()} as first
  83.      * argument.
  84.      * </p>
  85.      * @param resource name of the zip file to browse
  86.      */
  87.     public ZipJarCrawler(final String resource) {
  88.         this(ZipJarCrawler.class.getClassLoader(), resource);
  89.     }

  90.     /** Build a zip crawler for an archive file in classpath.
  91.      * @param classLoader class loader to use to retrieve the resources
  92.      * @param resource name of the zip file to browse
  93.      */
  94.     public ZipJarCrawler(final ClassLoader classLoader, final String resource) {
  95.         try {
  96.             this.file        = null;
  97.             this.resource    = resource;
  98.             this.classLoader = classLoader;
  99.             this.url         = null;
  100.             this.name        = classLoader.getResource(resource).toURI().toString();
  101.         } catch (URISyntaxException use) {
  102.             throw new OrekitException(use, LocalizedCoreFormats.SIMPLE_MESSAGE, use.getMessage());
  103.         }
  104.     }

  105.     /** Build a zip crawler for an archive file on network.
  106.      * @param url URL of the zip file on network
  107.      */
  108.     public ZipJarCrawler(final URL url) {
  109.         try {
  110.             this.file        = null;
  111.             this.resource    = null;
  112.             this.classLoader = null;
  113.             this.url         = url;
  114.             this.name        = url.toURI().toString();
  115.         } catch (URISyntaxException use) {
  116.             throw new OrekitException(use, LocalizedCoreFormats.SIMPLE_MESSAGE, use.getMessage());
  117.         }
  118.     }

  119.     /** {@inheritDoc} */
  120.     public boolean feed(final Pattern supported,
  121.                         final DataLoader visitor,
  122.                         final DataProvidersManager manager) {

  123.         try {

  124.             // open the raw data stream
  125.             try (InputStream in = openStream();
  126.                  Archive archive = new Archive(in)) {
  127.                 return feed(name, supported, visitor, manager, archive);
  128.             }

  129.         } catch (IOException | ParseException e) {
  130.             throw new OrekitException(e, new DummyLocalizable(e.getMessage()));
  131.         }

  132.     }

  133.     /**
  134.      * Open a stream to the raw archive.
  135.      *
  136.      * @return an open stream.
  137.      * @throws IOException if the stream could not be opened.
  138.      */
  139.     private InputStream openStream() throws IOException {
  140.         if (file != null) {
  141.             return new FileInputStream(file);
  142.         } else if (resource != null) {
  143.             return classLoader.getResourceAsStream(resource);
  144.         } else {
  145.             return url.openConnection().getInputStream();
  146.         }
  147.     }

  148.     /** Feed a data file loader by browsing the entries in a zip/jar.
  149.      * @param prefix prefix to use for name
  150.      * @param supported pattern for file names supported by the visitor
  151.      * @param visitor data file visitor to use
  152.      * @param manager used for filtering data.
  153.      * @param archive archive to read
  154.      * @return true if something has been loaded
  155.      * @exception IOException if data cannot be read
  156.      * @exception ParseException if data cannot be read
  157.      */
  158.     private boolean feed(final String prefix,
  159.                          final Pattern supported,
  160.                          final DataLoader visitor,
  161.                          final DataProvidersManager manager,
  162.                          final Archive archive)
  163.         throws IOException, ParseException {

  164.         OrekitException delayedException = null;
  165.         boolean loaded = false;

  166.         // loop over all entries
  167.         for (final Archive.EntryStream entry : archive) {

  168.             try {

  169.                 if (visitor.stillAcceptsData() && !entry.isDirectory()) {

  170.                     final String fullName = prefix + "!/" + entry.getName();

  171.                     if (ZIP_ARCHIVE_PATTERN.matcher(entry.getName()).matches()) {

  172.                         // recurse inside the archive entry
  173.                         loaded = feed(fullName, supported, visitor, manager, new Archive(entry)) || loaded;

  174.                     } else {

  175.                         // remove leading directories
  176.                         String entryName = entry.getName();
  177.                         final int lastSlash = entryName.lastIndexOf('/');
  178.                         if (lastSlash >= 0) {
  179.                             entryName = entryName.substring(lastSlash + 1);
  180.                         }

  181.                         // apply all registered filters
  182.                         DataSource data = new DataSource(entryName, () -> entry);
  183.                         data = manager.getFiltersManager().applyRelevantFilters(data);

  184.                         if (supported.matcher(data.getName()).matches()) {
  185.                             // visit the current file
  186.                             try (InputStream input = data.getOpener().openStreamOnce()) {
  187.                                 visitor.loadData(input, fullName);
  188.                                 loaded = true;
  189.                             }
  190.                         }

  191.                     }

  192.                 }

  193.             } catch (OrekitException oe) {
  194.                 delayedException = oe;
  195.             }

  196.             entry.close();

  197.         }

  198.         if (!loaded && delayedException != null) {
  199.             throw delayedException;
  200.         }
  201.         return loaded;

  202.     }

  203.     /** Local class wrapping a zip archive. */
  204.     private static final class Archive implements Closeable, Iterable<Archive.EntryStream> {

  205.         /** Zip stream. */
  206.         private final ZipInputStream zip;

  207.         /** Next entry. */
  208.         private EntryStream next;

  209.         /** Simple constructor.
  210.          * @param rawStream raw stream
  211.          * @exception IOException if first entry cannot be retrieved
  212.          */
  213.         Archive(final InputStream rawStream) throws IOException {
  214.             zip = new ZipInputStream(rawStream);
  215.             goToNext();
  216.         }

  217.         /** Go to next entry.
  218.         * @exception IOException if next entry cannot be retrieved
  219.          */
  220.         private void goToNext() throws IOException {
  221.             final ZipEntry ze = zip.getNextEntry();
  222.             if (ze == null) {
  223.                 next = null;
  224.             } else {
  225.                 next = new EntryStream(ze.getName(), ze.isDirectory());
  226.             }
  227.         }

  228.         /** {@inheritDoc} */
  229.         @Override
  230.         public Iterator<Archive.EntryStream> iterator() {
  231.             return new Iterator<EntryStream> () {

  232.                 /** {@inheritDoc} */
  233.                 @Override
  234.                 public boolean hasNext() {
  235.                     return next != null;
  236.                 }

  237.                 /** {@inheritDoc} */
  238.                 @Override
  239.                 public EntryStream next() throws NoSuchElementException {
  240.                     if (next == null) {
  241.                         // this should never happen
  242.                         throw new NoSuchElementException();
  243.                     }
  244.                     return next;
  245.                 }

  246.             };
  247.         }

  248.         /** {@inheritDoc} */
  249.         @Override
  250.         public void close() throws IOException {
  251.             zip.close();
  252.         }

  253.         /** Archive entry. */
  254.         public class EntryStream extends InputStream {

  255.             /** Name of the entry. */
  256.             private final String name;

  257.             /** Directory indicator. */
  258.             private boolean isDirectory;

  259.             /** Indicator for already closed stream. */
  260.             private boolean closed;

  261.             /** Simple constructor.
  262.              * @param name name of the entry
  263.              * @param isDirectory if true, the entry is a directory
  264.              */
  265.             EntryStream(final String name, final boolean isDirectory) {
  266.                 this.name        = name;
  267.                 this.isDirectory = isDirectory;
  268.                 this.closed      = false;
  269.             }

  270.             /** Get the name of the entry.
  271.              * @return name of the entry
  272.              */
  273.             public String getName() {
  274.                 return name;
  275.             }

  276.             /** Check if the entry is a directory.
  277.              * @return true if the entry is a directory
  278.              */
  279.             public boolean isDirectory() {
  280.                 return isDirectory;
  281.             }

  282.             /** {@inheritDoc} */
  283.             @Override
  284.             public int read() throws IOException {
  285.                 // delegate read to global input stream
  286.                 return zip.read();
  287.             }

  288.             /** {@inheritDoc} */
  289.             @Override
  290.             public void close() throws IOException {
  291.                 if (!closed) {
  292.                     zip.closeEntry();
  293.                     goToNext();
  294.                     closed = true;
  295.                 }
  296.             }

  297.             @Override
  298.             public int available() throws IOException {
  299.                 return zip.available();
  300.             }

  301.             @Override
  302.             public int read(final byte[] b, final int off, final int len)
  303.                     throws IOException {
  304.                 return zip.read(b, off, len);
  305.             }

  306.             @Override
  307.             public long skip(final long n) throws IOException {
  308.                 return zip.skip(n);
  309.             }

  310.             @Override
  311.             public boolean markSupported() {
  312.                 return zip.markSupported();
  313.             }

  314.             @Override
  315.             public void mark(final int readlimit) {
  316.                 zip.mark(readlimit);
  317.             }

  318.             @Override
  319.             public void reset() throws IOException {
  320.                 zip.reset();
  321.             }

  322.             @Override
  323.             public int read(final byte[] b) throws IOException {
  324.                 return zip.read(b);
  325.             }

  326.         }

  327.     }

  328. }