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