1   /* Copyright 2002-2020 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  
19  import java.io.Closeable;
20  import java.io.File;
21  import java.io.FileInputStream;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.net.URISyntaxException;
25  import java.net.URL;
26  import java.text.ParseException;
27  import java.util.Iterator;
28  import java.util.NoSuchElementException;
29  import java.util.regex.Pattern;
30  import java.util.zip.ZipEntry;
31  import java.util.zip.ZipInputStream;
32  
33  import org.hipparchus.exception.DummyLocalizable;
34  import org.hipparchus.exception.LocalizedCoreFormats;
35  import org.orekit.annotation.DefaultDataContext;
36  import org.orekit.errors.OrekitException;
37  
38  
39  /** Helper class for loading data files from a zip/jar archive.
40   * <p>
41   * This class browses all entries in a zip/jar archive in filesystem or in classpath.
42   * </p>
43   * <p>
44   * The organization of entries within the archive is unspecified. All entries are
45   * checked in turn. If several entries of the archive are supported by the data
46   * loader, all of them will be loaded.
47   * </p>
48   * <p>
49   * All {@link DataProvidersManager#addFilter(DataFilter) registered}
50   * {@link DataFilter filters} are applied.
51   * </p>
52   * <p>
53   * Zip archives entries are supported recursively.
54   * </p>
55   * <p>
56   * This is a simple application of the <code>visitor</code> design pattern for
57   * zip entries browsing.
58   * </p>
59   * @see DataProvidersManager
60   * @author Luc Maisonobe
61   */
62  public class ZipJarCrawler implements DataProvider {
63  
64      /** Zip archive on the filesystem. */
65      private final File file;
66  
67      /** Zip archive in the classpath. */
68      private final String resource;
69  
70      /** Class loader to use. */
71      private final ClassLoader classLoader;
72  
73      /** Zip archive on network. */
74      private final URL url;
75  
76      /** Prefix name of the zip. */
77      private final String name;
78  
79      /** Build a zip crawler for an archive file on filesystem.
80       * @param file zip file to browse
81       */
82      public ZipJarCrawler(final File file) {
83          this.file        = file;
84          this.resource    = null;
85          this.classLoader = null;
86          this.url         = null;
87          this.name        = file.getAbsolutePath();
88      }
89  
90      /** Build a zip crawler for an archive file in classpath.
91       * <p>
92       * Calling this constructor has the same effect as calling
93       * {@link #ZipJarCrawler(ClassLoader, String)} with
94       * {@code ZipJarCrawler.class.getClassLoader()} as first
95       * argument.
96       * </p>
97       * @param resource name of the zip file to browse
98       */
99      public ZipJarCrawler(final String resource) {
100         this(ZipJarCrawler.class.getClassLoader(), resource);
101     }
102 
103     /** Build a zip crawler for an archive file in classpath.
104      * @param classLoader class loader to use to retrieve the resources
105      * @param resource name of the zip file to browse
106      */
107     public ZipJarCrawler(final ClassLoader classLoader, final String resource) {
108         try {
109             this.file        = null;
110             this.resource    = resource;
111             this.classLoader = classLoader;
112             this.url         = null;
113             this.name        = classLoader.getResource(resource).toURI().toString();
114         } catch (URISyntaxException use) {
115             throw new OrekitException(use, LocalizedCoreFormats.SIMPLE_MESSAGE, use.getMessage());
116         }
117     }
118 
119     /** Build a zip crawler for an archive file on network.
120      * @param url URL of the zip file on network
121      */
122     public ZipJarCrawler(final URL url) {
123         try {
124             this.file        = null;
125             this.resource    = null;
126             this.classLoader = null;
127             this.url         = url;
128             this.name        = url.toURI().toString();
129         } catch (URISyntaxException use) {
130             throw new OrekitException(use, LocalizedCoreFormats.SIMPLE_MESSAGE, use.getMessage());
131         }
132     }
133 
134     @Override
135     @Deprecated
136     @DefaultDataContext
137     public boolean feed(final Pattern supported, final DataLoader visitor) {
138         return feed(supported, visitor, DataContext.getDefault().getDataProvidersManager());
139     }
140 
141     /** {@inheritDoc} */
142     public boolean feed(final Pattern supported,
143                         final DataLoader visitor,
144                         final DataProvidersManager manager) {
145 
146         try {
147 
148             // open the raw data stream
149             try (InputStream in = openStream();
150                  Archive archive = new Archive(in)) {
151                 return feed(name, supported, visitor, manager, archive);
152             }
153 
154         } catch (IOException | ParseException e) {
155             throw new OrekitException(e, new DummyLocalizable(e.getMessage()));
156         }
157 
158     }
159 
160     /**
161      * Open a stream to the raw archive.
162      *
163      * @return an open stream.
164      * @throws IOException if the stream could not be opened.
165      */
166     private InputStream openStream() throws IOException {
167         if (file != null) {
168             return new FileInputStream(file);
169         } else if (resource != null) {
170             return classLoader.getResourceAsStream(resource);
171         } else {
172             return url.openConnection().getInputStream();
173         }
174     }
175 
176     /** Feed a data file loader by browsing the entries in a zip/jar.
177      * @param prefix prefix to use for name
178      * @param supported pattern for file names supported by the visitor
179      * @param visitor data file visitor to use
180      * @param manager used for filtering data.
181      * @param archive archive to read
182      * @return true if something has been loaded
183      * @exception IOException if data cannot be read
184      * @exception ParseException if data cannot be read
185      */
186     private boolean feed(final String prefix,
187                          final Pattern supported,
188                          final DataLoader visitor,
189                          final DataProvidersManager manager,
190                          final Archive archive)
191         throws IOException, ParseException {
192 
193         OrekitException delayedException = null;
194         boolean loaded = false;
195 
196         // loop over all entries
197         for (final Archive.EntryStream entry : archive) {
198 
199             try {
200 
201                 if (visitor.stillAcceptsData() && !entry.isDirectory()) {
202 
203                     final String fullName = prefix + "!/" + entry.getName();
204 
205                     if (ZIP_ARCHIVE_PATTERN.matcher(entry.getName()).matches()) {
206 
207                         // recurse inside the archive entry
208                         loaded = feed(fullName, supported, visitor, manager, new Archive(entry)) || loaded;
209 
210                     } else {
211 
212                         // remove leading directories
213                         String entryName = entry.getName();
214                         final int lastSlash = entryName.lastIndexOf('/');
215                         if (lastSlash >= 0) {
216                             entryName = entryName.substring(lastSlash + 1);
217                         }
218 
219                         // apply all registered filters
220                         NamedDataamedData">NamedData data = new NamedData(entryName, () -> entry);
221                         data = manager.applyAllFilters(data);
222 
223                         if (supported.matcher(data.getName()).matches()) {
224                             // visit the current file
225                             try (InputStream input = data.getStreamOpener().openStream()) {
226                                 visitor.loadData(input, fullName);
227                                 loaded = true;
228                             }
229                         }
230 
231                     }
232 
233                 }
234 
235             } catch (OrekitException oe) {
236                 delayedException = oe;
237             }
238 
239             entry.close();
240 
241         }
242 
243         if (!loaded && delayedException != null) {
244             throw delayedException;
245         }
246         return loaded;
247 
248     }
249 
250     /** Local class wrapping a zip archive. */
251     private static final class Archive implements Closeable, Iterable<Archive.EntryStream> {
252 
253         /** Zip stream. */
254         private final ZipInputStream zip;
255 
256         /** Next entry. */
257         private EntryStream next;
258 
259         /** Simple constructor.
260          * @param rawStream raw stream
261          * @exception IOException if first entry cannot be retrieved
262          */
263         Archive(final InputStream rawStream) throws IOException {
264             zip = new ZipInputStream(rawStream);
265             goToNext();
266         }
267 
268         /** Go to next entry.
269         * @exception IOException if next entry cannot be retrieved
270          */
271         private void goToNext() throws IOException {
272             final ZipEntry ze = zip.getNextEntry();
273             if (ze == null) {
274                 next = null;
275             } else {
276                 next = new EntryStream(ze.getName(), ze.isDirectory());
277             }
278         }
279 
280         /** {@inheritDoc} */
281         @Override
282         public Iterator<Archive.EntryStream> iterator() {
283             return new Iterator<EntryStream> () {
284 
285                 /** {@inheritDoc} */
286                 @Override
287                 public boolean hasNext() {
288                     return next != null;
289                 }
290 
291                 /** {@inheritDoc} */
292                 @Override
293                 public EntryStream next() throws NoSuchElementException {
294                     if (next == null) {
295                         // this should never happen
296                         throw new NoSuchElementException();
297                     }
298                     return next;
299                 }
300 
301             };
302         }
303 
304         /** {@inheritDoc} */
305         @Override
306         public void close() throws IOException {
307             zip.close();
308         }
309 
310         /** Archive entry. */
311         public class EntryStream extends InputStream {
312 
313             /** Name of the entry. */
314             private final String name;
315 
316             /** Directory indicator. */
317             private boolean isDirectory;
318 
319             /** Indicator for already closed stream. */
320             private boolean closed;
321 
322             /** Simple constructor.
323              * @param name name of the entry
324              * @param isDirectory if true, the entry is a directory
325              */
326             EntryStream(final String name, final boolean isDirectory) {
327                 this.name        = name;
328                 this.isDirectory = isDirectory;
329                 this.closed      = false;
330             }
331 
332             /** Get the name of the entry.
333              * @return name of the entry
334              */
335             public String getName() {
336                 return name;
337             }
338 
339             /** Check if the entry is a directory.
340              * @return true if the entry is a directory
341              */
342             public boolean isDirectory() {
343                 return isDirectory;
344             }
345 
346             /** {@inheritDoc} */
347             @Override
348             public int read() throws IOException {
349                 // delegate read to global input stream
350                 return zip.read();
351             }
352 
353             /** {@inheritDoc} */
354             @Override
355             public void close() throws IOException {
356                 if (!closed) {
357                     zip.closeEntry();
358                     goToNext();
359                     closed = true;
360                 }
361             }
362 
363             @Override
364             public int available() throws IOException {
365                 return zip.available();
366             }
367 
368             @Override
369             public int read(final byte[] b, final int off, final int len)
370                     throws IOException {
371                 return zip.read(b, off, len);
372             }
373 
374             @Override
375             public long skip(final long n) throws IOException {
376                 return zip.skip(n);
377             }
378 
379             @Override
380             public boolean markSupported() {
381                 return zip.markSupported();
382             }
383 
384             @Override
385             public void mark(final int readlimit) {
386                 zip.mark(readlimit);
387             }
388 
389             @Override
390             public void reset() throws IOException {
391                 zip.reset();
392             }
393 
394             @Override
395             public int read(final byte[] b) throws IOException {
396                 return zip.read(b);
397             }
398 
399         }
400 
401     }
402 
403 }