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 }