1   /* Copyright 2002-2022 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.File;
20  import java.io.FileInputStream;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.InputStreamReader;
24  import java.io.Reader;
25  import java.net.URI;
26  import java.nio.ByteBuffer;
27  import java.nio.CharBuffer;
28  import java.nio.charset.StandardCharsets;
29  import java.nio.file.Files;
30  import java.nio.file.Paths;
31  
32  /** Container associating a name with a stream or reader that can be opened <em>lazily</em>.
33   * <p>
34   * This association and the lazy-opening are useful in different cases:
35   * <ul>
36   *   <li>when {@link DirectoryCrawler crawling} a directory tree to select data
37   *   to be loaded by a {@link DataLoader}, the files that are not meaningful for
38   *   the loader can be ignored and not opened at all</li>
39   *   <li>when {@link DataFilter data filtering} is used, the raw stream can
40   *   be opened by the filter only if the upper level filtered stream is opened</li>
41   *   <li>when opening a stream for loading the data it provides, the opening
42   *   and closing actions can be grouped in Orekit internal code using a {@code try
43   *   with resources} clause so closing is done properly even in case of exception</li>
44   *   <li>if some pre-reading of the first few bytes or characters are needed to decide how to
45   *   load data (as in {@link org.orekit.files.ccsds.utils.lexical.LexicalAnalyzerSelector}),
46   *   then the stream can be opened, buffered and rewound and a fake open method used
47   *   to return the already open stream so a {@code try with resources} clause
48   *   elsewhere works properly for closing the stream</li>
49   * </ul>
50   * <p>
51   * Beware that the purpose of this class is only to delay this opening (or not open
52   * the stream or reader at all), it is <em>not</em> intended to open the stream several
53   * times and <em>not</em> intended to open both the binary stream and the characters reader.
54   * Some implementations may fail if the {@link #getOpener() opener}'s
55   * {@link Opener#openStreamOnce() openStreamOnce} or {@link Opener#openReaderOnce() openReaderOnce}
56   * methods are called several times or are both called separately. This is particularly
57   * true for network-based streams.
58   * </p>
59   * @see DataFilter
60   * @author Luc Maisonobe
61   * @since 9.2
62   */
63  public class DataSource {
64  
65      /** Name of the data (file name, zip entry name...). */
66      private final String name;
67  
68      /** Supplier for data stream. */
69      private final Opener opener;
70  
71      /** Complete constructor.
72       * @param name data name
73       * @param streamOpener opener for the data stream
74       */
75      public DataSource(final String name, final StreamOpener streamOpener) {
76          this.name   = name;
77          this.opener = new BinaryBasedOpener(streamOpener);
78      }
79  
80      /** Complete constructor.
81       * @param name data name
82       * @param readerOpener opener for characters reader
83       */
84      public DataSource(final String name, final ReaderOpener readerOpener) {
85          this.name   = name;
86          this.opener = new ReaderBasedOpener(readerOpener);
87      }
88  
89      /** Build an instance from file name only.
90       * @param fileName name of the file
91       * @since 11.0
92       */
93      public DataSource(final String fileName) {
94          this(fileName, () -> Files.newInputStream(Paths.get(fileName)));
95      }
96  
97      /** Build an instance from a file on the local file system.
98       * @param file file
99       * @since 11.0
100      */
101     public DataSource(final File file) {
102         this(file.getName(), () -> new FileInputStream(file));
103     }
104 
105     /** Build an instance from URI only.
106      * @param uri URI of the file
107      * @since 11.0
108      */
109     public DataSource(final URI uri) {
110         this(Paths.get(uri).toFile());
111     }
112 
113     /** Get the name of the data.
114      * @return name of the data
115      */
116     public String getName() {
117         return name;
118     }
119 
120     /** Get the data stream opener.
121      * @return data stream opener
122      */
123     public Opener getOpener() {
124         return opener;
125     }
126 
127     /** Interface for lazy-opening a binary stream one time. */
128     public interface StreamOpener {
129         /** Open the stream once.
130          * <p>
131          * Beware that this interface is only intended for <em>lazy</em> opening a
132          * stream, i.e. to delay this opening (or not open the stream at all).
133          * It is <em>not</em> intended to open the stream several times. Some
134          * implementations may fail if an attempt to open a stream several
135          * times is made. This is particularly true for network-based streams.
136          * </p>
137          * @return opened stream
138          * @exception IOException if stream cannot be opened
139          */
140         InputStream openOnce() throws IOException;
141 
142     }
143 
144     /** Interface for lazy-opening a characters stream one time. */
145     public interface ReaderOpener {
146         /** Open the stream once.
147          * <p>
148          * Beware that this interface is only intended for <em>lazy</em> opening a
149          * stream, i.e. to delay this opening (or not open the stream at all).
150          * It is <em>not</em> intended to open the stream several times. Some
151          * implementations may fail if an attempt to open a stream several
152          * times is made. This is particularly true for network-based streams.
153          * </p>
154          * @return opened stream
155          * @exception IOException if stream cannot be opened
156          */
157         Reader openOnce() throws IOException;
158 
159     }
160 
161     /** Interface for lazy-opening data streams one time. */
162     public interface Opener {
163 
164         /** Check if the raw data is binary.
165          * <p>
166          * The raw data may be either binary or characters. In both cases,
167          * either {@link #openStreamOnce()} or {@link #openReaderOnce()} may
168          * be called, but one will be more efficient than the other as one
169          * will supply data as is and the other one will convert raw data
170          * before providing it. If conversion is needed, it will also be done
171          * using {@link StandardCharsets#UTF_8 UTF8 encoding}, which may not
172          * be suitable. This method helps the data consumer to either choose
173          * the more efficient method or avoid wrong encoding conversion.
174          * </p>
175          * @return true if raw data is binary, false if raw data is characters
176          */
177         boolean rawDataIsBinary();
178 
179         /** Open a bytes stream once.
180          * <p>
181          * Beware that this interface is only intended for <em>lazy</em> opening a
182          * stream, i.e. to delay this opening (or not open the stream at all).
183          * It is <em>not</em> intended to open the stream several times and not
184          * intended to open both the {@link #openStreamOnce() binary stream} and
185          * the {@link #openReaderOnce() characters stream} separately (but opening
186          * the reader may be implemented by opening the binary stream or vice-versa).
187          * Implementations may fail if an attempt to open a stream several times is
188          * made. This is particularly true for network-based streams.
189          * </p>
190          * @return opened stream or null if there are no data streams at all
191          * @exception IOException if stream cannot be opened
192          */
193         InputStream openStreamOnce() throws IOException;
194 
195         /** Open a characters stream reader once.
196          * <p>
197          * Beware that this interface is only intended for <em>lazy</em> opening a
198          * stream, i.e. to delay this opening (or not open the stream at all).
199          * It is <em>not</em> intended to open the stream several times and not
200          * intended to open both the {@link #openStreamOnce() binary stream} and
201          * the {@link #openReaderOnce() characters stream} separately (but opening
202          * the reader may be implemented by opening the binary stream or vice-versa).
203          * Implementations may fail if an attempt to open a stream several times is
204          * made. This is particularly true for network-based streams.
205          * </p>
206          * @return opened reader or null if there are no data streams at all
207          * @exception IOException if stream cannot be opened
208          */
209         Reader openReaderOnce() throws IOException;
210 
211     }
212 
213     /** Opener based on a binary stream. */
214     private static class BinaryBasedOpener implements Opener {
215 
216         /** Opener for the data stream. */
217         private final StreamOpener streamOpener;
218 
219         /** Simple constructor.
220          * @param streamOpener opener for the data stream
221          */
222         BinaryBasedOpener(final StreamOpener streamOpener) {
223             this.streamOpener = streamOpener;
224         }
225 
226         /** {@inheritDoc} */
227         @Override
228         public boolean rawDataIsBinary() {
229             return true;
230         }
231 
232         /** {@inheritDoc} */
233         @Override
234         public InputStream openStreamOnce() throws IOException {
235             return streamOpener.openOnce();
236         }
237 
238         /** {@inheritDoc} */
239         @Override
240         public Reader openReaderOnce() throws IOException {
241             // convert bytes to characters
242             final InputStream is = openStreamOnce();
243             return (is == null) ? null : new InputStreamReader(is, StandardCharsets.UTF_8);
244         }
245 
246     }
247 
248     /** Opener based on a reader. */
249     private static class ReaderBasedOpener implements Opener {
250 
251         /** Size of the characters buffer. */
252         private static final int BUFFER_SIZE = 4096;
253 
254         /** Opener for characters reader. */
255         private final ReaderOpener readerOpener;
256 
257         /** Simple constructor.
258          * @param readerOpener opener for characters reader
259          */
260         ReaderBasedOpener(final ReaderOpener readerOpener) {
261             this.readerOpener = readerOpener;
262         }
263 
264         /** {@inheritDoc} */
265         @Override
266         public boolean rawDataIsBinary() {
267             return false;
268         }
269 
270         /** {@inheritDoc} */
271         @Override
272         public InputStream openStreamOnce() throws IOException {
273 
274             // open the underlying reader
275             final Reader reader = openReaderOnce();
276             if (reader == null) {
277                 return null;
278             }
279 
280             // set up a stream that convert characters to bytes
281             return new InputStream() {
282 
283                 private ByteBuffer buffer = null;
284 
285                 /** {@inheritDoc} */
286                 @Override
287                 public int read() throws IOException {
288                     if (buffer == null || !buffer.hasRemaining()) {
289                         // we need to refill the array
290 
291                         // get characters from the reader
292                         final CharBuffer cb = CharBuffer.allocate(BUFFER_SIZE);
293                         final int read = reader.read(cb);
294                         if (read < 0) {
295                             // end of data
296                             return read;
297                         }
298 
299                         // convert the characters read into bytes
300                         final int last = cb.position();
301                         cb.rewind();
302                         buffer = StandardCharsets.UTF_8.encode(cb.subSequence(0, last));
303 
304                     }
305 
306                     // return next byte
307                     return buffer.get();
308 
309                 }
310 
311             };
312         }
313 
314         /** {@inheritDoc} */
315         @Override
316         public Reader openReaderOnce() throws IOException {
317             return readerOpener.openOnce();
318         }
319 
320     }
321 
322 }
323