View Javadoc
1   /*
2    * #%L
3    * nom.tam FITS library
4    * %%
5    * Copyright (C) 1996 - 2024 nom-tam-fits
6    * %%
7    * This is free and unencumbered software released into the public domain.
8    *
9    * Anyone is free to copy, modify, publish, use, compile, sell, or
10   * distribute this software, either in source code form or as a compiled
11   * binary, for any purpose, commercial or non-commercial, and by any
12   * means.
13   *
14   * In jurisdictions that recognize copyright laws, the author or authors
15   * of this software dedicate any and all copyright interest in the
16   * software to the public domain. We make this dedication for the benefit
17   * of the public at large and to the detriment of our heirs and
18   * successors. We intend this dedication to be an overt act of
19   * relinquishment in perpetuity of all present and future rights to this
20   * software under copyright law.
21   *
22   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23   * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
24   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
25   * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
26   * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
27   * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
28   * OTHER DEALINGS IN THE SOFTWARE.
29   * #L%
30   */
31  
32  package nom.tam.util;
33  
34  import java.io.EOFException;
35  import java.io.IOException;
36  import java.nio.Buffer;
37  import java.nio.ByteBuffer;
38  import java.nio.ByteOrder;
39  
40  import nom.tam.fits.FitsFactory;
41  import nom.tam.util.type.ElementType;
42  
43  /**
44   * Efficient base class for encoding Java arrays into binary output (<i>primarily for internal use</i>)
45   *
46   * @author Attila Kovacs
47   *
48   * @since  1.16
49   *
50   * @see    InputDecoder
51   * @see    ArrayDataFile
52   * @see    ArrayInputStream
53   * @see    ArrayOutputStream
54   */
55  public abstract class OutputEncoder {
56  
57      /**
58       * The default local buffer size to use for encoding data into binary format
59       */
60      private static final int BUFFER_SIZE = FitsFactory.FITS_BLOCK_SIZE;
61  
62      /**
63       * The output to which to write encoded data (directly or from the conversion buffer)
64       */
65      protected OutputWriter out;
66  
67      /**
68       * Cumulative encoded byte count written to the output, including over-writes
69       */
70      private long count = 0;
71  
72      /**
73       * A local buffer for efficient conversions before bulk writing to the output
74       */
75      private OutputBuffer buf;
76  
77      /**
78       * Instantiates a new Java-to-binary encoder for arrays. To be used by subclass implementations only
79       *
80       * @see #setOutput(OutputWriter)
81       */
82      protected OutputEncoder() {
83          buf = new OutputBuffer(BUFFER_SIZE);
84      }
85  
86      /**
87       * Instantiates a new Java-to-binary encoder for arrays, writing encoded data to the specified output.
88       *
89       * @param o the output to which encoded data is to be written.
90       */
91      public OutputEncoder(OutputWriter o) {
92          this();
93          setOutput(o);
94      }
95  
96      /**
97       * Sets the output to which encoded data should be written (directly or from the conversion buffer).
98       *
99       * @param o the new output to which encoded data is to be written.
100      */
101     protected synchronized void setOutput(OutputWriter o) {
102         out = o;
103     }
104 
105     /**
106      * Returns the number of <i>encoded</i> bytes that were written to the output. It does not include bytes written
107      * directly (unencoded) to the output, such as via {@link #write(int)} or {@link #write(byte[], int, int)}.
108      *
109      * @return the number of encoded bytes written to the output.
110      *
111      * @see    RandomAccess#getFilePointer()
112      */
113     public synchronized long getCount() {
114         return count + buf.buffer.position();
115     }
116 
117     /**
118      * Returns the buffer that is used for conversion, which can be used to collate more elements for writing before
119      * bulk flushing data to the output (see {@link OutputBuffer#flush()}).
120      *
121      * @return the conversion buffer used by this encoder.
122      */
123     protected OutputBuffer getOutputBuffer() {
124         return buf;
125     }
126 
127     /**
128      * Makes sure that there is room in the conversion buffer for an upcoming element conversion, and flushes the buffer
129      * as necessary to make room. Subclass implementations should call this method before attempting a conversion
130      * operation.
131      *
132      * @param  bytes       the size of an element we will want to convert. It cannot exceed the size of the conversion
133      *                         buffer.
134      *
135      * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
136      *                         conversion.
137      */
138     void need(int bytes) throws IOException {
139         // TODO Once the deprecated {@link BufferEncoder} is retired, this
140         // should become
141         // a private method of OutputBuffer, with leading 'buf.' references
142         // stripped.
143         if (buf.buffer.remaining() < bytes) {
144             flush();
145         }
146     }
147 
148     /**
149      * Flushes the contents of the conversion buffer to the underlying output.
150      *
151      * @throws IOException if there was an IO error writing the contents of this buffer to the output.
152      */
153     protected synchronized void flush() throws IOException {
154         int n = buf.buffer.position();
155         out.write(buf.data, 0, n);
156         count += n;
157         buf.rewind();
158     }
159 
160     /**
161      * Writes a byte directly to the output. See the general contract of {@link java.io.DataOutputStream#write(int)}.
162      * Unencoded bytes written by this method are not reflected in the value returned by {@link #getCount()}.
163      *
164      * @param  b           the (unsigned) byte value to write.
165      *
166      * @throws IOException if there was an underlying IO error
167      *
168      * @see                java.io.DataOutputStream#write(int)
169      */
170     protected synchronized void write(int b) throws IOException {
171         flush();
172         out.write(b);
173     }
174 
175     /**
176      * Writes up to the specified number of bytes from a buffer directly to the output. See the general contract of
177      * {@link java.io.DataOutputStream#write(byte[], int, int)}. The number of unencoded bytes written by this method
178      * are not reflected in the value returned by {@link #getCount()}.
179      *
180      * @param  b           the buffer
181      * @param  start       the starting buffer index
182      * @param  length      the number of bytes to write.
183      *
184      * @throws IOException if there was an underlying IO error
185      *
186      * @see                java.io.DataOutputStream#write(byte[], int, int)
187      */
188     protected synchronized void write(byte[] b, int start, int length) throws IOException {
189         flush();
190         out.write(b, start, length);
191     }
192 
193     /**
194      * Writes the contents of a Java array to the output translating the data to the required binary representation. The
195      * argument may be any generic Java array, including heterogeneous arrays of arrays.
196      *
197      * @param  o                        the Java array, including heterogeneous arrays of arrays. If <code>null</code>
198      *                                      nothing will be written to the output.
199      *
200      * @throws IOException              if there was an IO error writing to the output
201      * @throws IllegalArgumentException if the supplied object is not a Java array or if it contains Java types that are
202      *                                      not supported by the decoder.
203      *
204      * @see                             ArrayDataOutput#writeArray(Object)
205      */
206     public abstract void writeArray(Object o) throws IOException, IllegalArgumentException;
207 
208     /**
209      * <p>
210      * The conversion buffer for encoding Java arrays (objects) into a binary data representation.
211      * </p>
212      * <p>
213      * The buffering is most efficient if multiple conversions (put methods) are collated before a forced
214      * {@link #flush()} call to the output. The caller need not worry about space remaining in the buffer. As new data
215      * is placed (put) into the buffer, the buffer will automatically flush the contents to the output to make space for
216      * new elements as it goes. The caller only needs to call the final {@link #flush()}, to ensure that all elements
217      * bufferes so far are written to the output.
218      * </p>
219      *
220      * <pre>
221      * short[] shortArray = new short[100];
222      * float[] floaTarray = new float[48];
223      *
224      * // populate the arrays with data...
225      *
226      * // Convert to binary representation using the local
227      * // conversion buffer.
228      * ConversionBuffer buf = getBuffer();
229      *
230      * // Convert as much data as we want to the output format...
231      * buf.putDouble(1.0);
232      * buf.putInt(-1);
233      * buf.put(shortArray, 0, shortArray.length);
234      * buf.put(floatArray, 0, floatArray.length);
235      *
236      * // Once we are done with a chunk of data, we need to
237      * // make sure all it written to the output
238      * buf.flush();
239      * </pre>
240      *
241      * @author Attila Kovacs
242      */
243     protected final class OutputBuffer {
244 
245         /**
246          * the byte array that stores pending data to be written to the output
247          */
248         private final byte[] data;
249 
250         /** the buffer wrapped for NIO access */
251         private final ByteBuffer buffer;
252 
253         /** The current type-specific view of the buffer or null */
254         private Buffer view;
255 
256         private OutputBuffer(int size) {
257             data = new byte[size];
258             buffer = ByteBuffer.wrap(data);
259         }
260 
261         /**
262          * Sets the byte order of the binary representation to which data is encoded.
263          *
264          * @param order the new byte order
265          *
266          * @see         #byteOrder()
267          * @see         ByteBuffer#order(ByteOrder)
268          */
269         protected void setByteOrder(ByteOrder order) {
270             buffer.order(order);
271         }
272 
273         /**
274          * Returns the current byte order of the binary representation to which data is encoded.
275          *
276          * @return the byte order
277          *
278          * @see    #setByteOrder(ByteOrder)
279          * @see    ByteBuffer#order()
280          */
281         protected ByteOrder byteOrder() {
282             return buffer.order();
283         }
284 
285         private boolean isViewingAs(Class<? extends Buffer> type) {
286             if (view == null) {
287                 return false;
288             }
289             return type.isAssignableFrom(view.getClass());
290         }
291 
292         private void assertView(ElementType<?> type) {
293             if (!isViewingAs(type.bufferClass())) {
294                 view = type.asTypedBuffer(buffer);
295             }
296         }
297 
298         private void rewind() {
299             buffer.rewind();
300             view = null;
301         }
302 
303         /**
304          * Puts a single byte into the conversion buffer, making space for it as needed by flushing the current buffer
305          * contents to the output as necessary.
306          *
307          * @param  b           the byte value
308          *
309          * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
310          *                         conversion.
311          *
312          * @see                #flush()
313          */
314         protected void putByte(byte b) throws IOException {
315             need(1);
316             view = null;
317             buffer.put(b);
318         }
319 
320         /**
321          * Puts a 2-byte integer into the conversion buffer, making space for it as needed by flushing the current
322          * buffer contents to the output as necessary.
323          *
324          * @param  s           the 16-bit integer value
325          *
326          * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
327          *                         conversion.
328          *
329          * @see                #flush()
330          */
331         protected void putShort(short s) throws IOException {
332             need(Short.BYTES);
333             view = null;
334             buffer.putShort(s);
335         }
336 
337         /**
338          * Puts a 4-byte integer into the conversion buffer, making space for it as needed by flushing the current
339          * buffer contents to the output as necessary.
340          *
341          * @param  i           the 32-bit integer value
342          *
343          * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
344          *                         conversion.
345          *
346          * @see                #flush()
347          */
348         protected void putInt(int i) throws IOException {
349             need(Integer.BYTES);
350             view = null;
351             buffer.putInt(i);
352         }
353 
354         /**
355          * Puts an 8-byte integer into the conversion buffer, making space for it as needed by flushing the current
356          * buffer contents to the output as necessary.
357          *
358          * @param  l           the 64-bit integer value
359          *
360          * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
361          *                         conversion.
362          *
363          * @see                #flush()
364          */
365         protected void putLong(long l) throws IOException {
366             need(Long.BYTES);
367             view = null;
368             buffer.putLong(l);
369         }
370 
371         /**
372          * Puts an 4-byte single-precision floating point value into the conversion buffer, making space for it as
373          * needed by flushing the current buffer contents to the output as necessary.
374          *
375          * @param  f           the 32-bit single-precision floating point value
376          *
377          * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
378          *                         conversion.
379          *
380          * @see                #flush()
381          */
382         protected void putFloat(float f) throws IOException {
383             need(Float.BYTES);
384             view = null;
385             buffer.putFloat(f);
386         }
387 
388         /**
389          * Puts an 8-byte double-precision floating point value into the conversion buffer, making space for it as
390          * needed by flushing the current buffer contents to the output as necessary.
391          *
392          * @param  d           the 64-bit double-precision floating point value
393          *
394          * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
395          *                         conversion.
396          *
397          * @see                #flush()
398          */
399         protected void putDouble(double d) throws IOException {
400             need(Double.BYTES);
401             view = null;
402             buffer.putDouble(d);
403         }
404 
405         /**
406          * Puts an array of bytes into the conversion buffer, flushing the buffer intermittently as necessary to make
407          * room as it goes.
408          *
409          * @param  src         an array of byte values
410          * @param  start       the index of the first element to convert
411          * @param  length      the number of elements to convert
412          *
413          * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
414          *                         conversion.
415          */
416         protected void put(byte[] src, int start, int length) throws IOException {
417             if (length == 1) {
418                 need(1);
419                 buffer.put(src[start]);
420                 return;
421             }
422 
423             view = null;
424 
425             int got = 0;
426 
427             while (got < length) {
428                 need(1);
429                 int m = Math.min(length - got, buffer.remaining());
430                 buffer.put(src, start + got, m);
431                 got += m;
432             }
433         }
434 
435         /**
436          * Puts an array of 16-bit integers into the conversion buffer, flushing the buffer intermittently as necessary
437          * to make room as it goes.
438          *
439          * @param  src         an array of 16-bit integer values
440          * @param  start       the index of the first element to convert
441          * @param  length      the number of elements to convert
442          *
443          * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
444          *                         conversion.
445          */
446         protected void put(short[] src, int start, int length) throws IOException {
447             if (length == 1 && !isViewingAs(ElementType.SHORT.bufferClass())) {
448                 putShort(src[start]);
449             } else {
450                 put(ElementType.SHORT, src, start, length);
451             }
452         }
453 
454         /**
455          * Puts an array of 32-bit integers into the conversion buffer, flushing the buffer intermittently as necessary
456          * to make room as it goes.
457          *
458          * @param  src         an array of 32-bit integer values
459          * @param  start       the index of the first element to convert
460          * @param  length      the number of elements to convert
461          *
462          * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
463          *                         conversion.
464          */
465         protected void put(int[] src, int start, int length) throws IOException {
466             if (length == 1 && !isViewingAs(ElementType.INT.bufferClass())) {
467                 putInt(src[start]);
468             } else {
469                 put(ElementType.INT, src, start, length);
470             }
471         }
472 
473         /**
474          * Puts an array of 64-bit integers into the conversion buffer, flushing the buffer intermittently as necessary
475          * to make room as it goes.
476          *
477          * @param  src         an array of 64-bit integer values
478          * @param  start       the index of the first element to convert
479          * @param  length      the number of elements to convert
480          *
481          * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
482          *                         conversion.
483          */
484         protected void put(long[] src, int start, int length) throws IOException {
485             if (length == 1 && !isViewingAs(ElementType.LONG.bufferClass())) {
486                 putLong(src[start]);
487             } else {
488                 put(ElementType.LONG, src, start, length);
489             }
490         }
491 
492         /**
493          * Puts an array of 32-bit single-precision floating point values into the conversion buffer, flushing the
494          * buffer intermittently as necessary to make room as it goes.
495          *
496          * @param  src         an array of 32-bit single-precision floating point values
497          * @param  start       the index of the first element to convert
498          * @param  length      the number of elements to convert
499          *
500          * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
501          *                         conversion.
502          */
503         protected void put(float[] src, int start, int length) throws IOException {
504             if (length == 1 && !isViewingAs(ElementType.FLOAT.bufferClass())) {
505                 putFloat(src[start]);
506             } else {
507                 put(ElementType.FLOAT, src, start, length);
508             }
509         }
510 
511         /**
512          * Puts an array of 64-bit double-precision floating point values into the conversion buffer, flushing the
513          * buffer intermittently as necessary to make room as it goes.
514          *
515          * @param  src         an array of 64-bit double-precision floating point values
516          * @param  start       the index of the first element to convert
517          * @param  length      the number of elements to convert
518          *
519          * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
520          *                         conversion.
521          */
522         protected void put(double[] src, int start, int length) throws IOException {
523             if (length == 1 && !isViewingAs(ElementType.DOUBLE.bufferClass())) {
524                 putDouble(src[start]);
525             } else {
526                 put(ElementType.DOUBLE, src, start, length);
527             }
528         }
529 
530         /**
531          * Puts an array of 64-bit values into the conversion buffer, flushing the buffer intermittently as necessary to
532          * make room as it goes.
533          *
534          * @param  the         FITS element type of the the 1D array
535          * @param  src         a 1D array of values of the specified element type
536          * @param  start       the index of the first element to convert
537          * @param  length      the number of elements to convert
538          *
539          * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
540          *                         conversion.
541          */
542         @SuppressWarnings("unchecked")
543         private <B extends Buffer> void put(ElementType<B> e, Object dst, int from, int n)
544                 throws EOFException, IOException {
545             int got = 0;
546 
547             while (got < n) {
548                 need(e.size());
549                 assertView(e);
550                 int m = Math.min(n - got, view.remaining());
551                 e.putArray((B) view, dst, from + got, m);
552                 buffer.position(buffer.position() + m * e.size());
553                 got += m;
554             }
555         }
556     }
557 }