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 }