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