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      /** For thread synchronization */
78      protected Object lock = new Object();
79  
80      /**
81       * Instantiates a new Java-to-binary encoder for arrays. To be used by subclass implementations only
82       *
83       * @see #setOutput(OutputWriter)
84       */
85      protected OutputEncoder() {
86          buf = new OutputBuffer(BUFFER_SIZE);
87      }
88  
89      /**
90       * Instantiates a new Java-to-binary encoder for arrays, writing encoded data to the specified output.
91       *
92       * @param o the output to which encoded data is to be written.
93       */
94      public OutputEncoder(OutputWriter o) {
95          this();
96          setOutput(o);
97      }
98  
99      /**
100      * Sets the output to which encoded data should be written (directly or from the conversion buffer).
101      *
102      * @param o the new output to which encoded data is to be written.
103      */
104     protected void setOutput(OutputWriter o) {
105         synchronized (lock) {
106             out = o;
107         }
108     }
109 
110     /**
111      * Returns the number of <i>encoded</i> bytes that were written to the output. It does not include bytes written
112      * directly (unencoded) to the output, such as via {@link #write(int)} or {@link #write(byte[], int, int)}.
113      *
114      * @return the number of encoded bytes written to the output.
115      *
116      * @see    RandomAccess#getFilePointer()
117      */
118     public long getCount() {
119         synchronized (lock) {
120             return count + buf.buffer.position();
121         }
122     }
123 
124     /**
125      * Returns the buffer that is used for conversion, which can be used to collate more elements for writing before
126      * bulk flushing data to the output (see {@link #flush()}).
127      *
128      * @return the conversion buffer used by this encoder.
129      */
130     protected OutputBuffer getOutputBuffer() {
131         return buf;
132     }
133 
134     /**
135      * Makes sure that there is room in the conversion buffer for an upcoming element conversion, and flushes the buffer
136      * as necessary to make room. Subclass implementations should call this method before attempting a conversion
137      * operation.
138      *
139      * @param  bytes       the size of an element we will want to convert. It cannot exceed the size of the conversion
140      *                         buffer.
141      *
142      * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
143      *                         conversion.
144      */
145     void need(int bytes) throws IOException {
146         // TODO Once the deprecated {@link BufferEncoder} is retired, this
147         // should become
148         // a private method of OutputBuffer, with leading 'buf.' references
149         // stripped.
150         if (buf.buffer.remaining() < bytes) {
151             flush();
152         }
153     }
154 
155     /**
156      * Flushes the contents of the conversion buffer to the underlying output.
157      *
158      * @throws IOException if there was an IO error writing the contents of this buffer to the output.
159      */
160     protected void flush() throws IOException {
161         synchronized (lock) {
162             int n = buf.buffer.position();
163             out.write(buf.data, 0, n);
164             count += n;
165             buf.rewind();
166         }
167     }
168 
169     /**
170      * Writes a byte directly to the output. See the general contract of {@link java.io.DataOutputStream#write(int)}.
171      * Unencoded bytes written by this method are not reflected in the value returned by {@link #getCount()}.
172      *
173      * @param  b           the (unsigned) byte value to write.
174      *
175      * @throws IOException if there was an underlying IO error
176      *
177      * @see                java.io.DataOutputStream#write(int)
178      */
179     protected void write(int b) throws IOException {
180         synchronized (lock) {
181             flush();
182             out.write(b);
183         }
184     }
185 
186     /**
187      * Writes up to the specified number of bytes from a buffer directly to the output. See the general contract of
188      * {@link java.io.DataOutputStream#write(byte[], int, int)}. The number of unencoded bytes written by this method
189      * are not reflected in the value returned by {@link #getCount()}.
190      *
191      * @param  b           the buffer
192      * @param  start       the starting buffer index
193      * @param  length      the number of bytes to write.
194      *
195      * @throws IOException if there was an underlying IO error
196      *
197      * @see                java.io.DataOutputStream#write(byte[], int, int)
198      */
199     protected void write(byte[] b, int start, int length) throws IOException {
200         synchronized (lock) {
201             flush();
202             out.write(b, start, length);
203         }
204     }
205 
206     /**
207      * Writes the contents of a Java array to the output translating the data to the required binary representation. The
208      * argument may be any generic Java array, including heterogeneous arrays of arrays.
209      *
210      * @param  o                        the Java array, including heterogeneous arrays of arrays. If <code>null</code>
211      *                                      nothing will be written to the output.
212      *
213      * @throws IOException              if there was an IO error writing to the output
214      * @throws IllegalArgumentException if the supplied object is not a Java array or if it contains Java types that are
215      *                                      not supported by the decoder.
216      *
217      * @see                             ArrayDataOutput#writeArray(Object)
218      */
219     public abstract void writeArray(Object o) throws IOException, IllegalArgumentException;
220 
221     /**
222      * <p>
223      * The conversion buffer for encoding Java arrays (objects) into a binary data representation.
224      * </p>
225      * <p>
226      * The buffering is most efficient if multiple conversions (put methods) are collated before a forced
227      * {@link #flush()} call to the output. The caller need not worry about space remaining in the buffer. As new data
228      * is placed (put) into the buffer, the buffer will automatically flush the contents to the output to make space for
229      * new elements as it goes. The caller only needs to call the final {@link #flush()}, to ensure that all elements
230      * buffered so far are written to the output.
231      * </p>
232      *
233      * <pre>
234      * short[] shortArray = new short[100];
235      * float[] floatTarray = new float[48];
236      *
237      * // populate the arrays with data...
238      *
239      * // Convert to binary representation using the local
240      * // conversion buffer.
241      * ConversionBuffer buf = getBuffer();
242      *
243      * // Convert as much data as we want to the output format...
244      * buf.putDouble(1.0);
245      * buf.putInt(-1);
246      * buf.put(shortArray, 0, shortArray.length);
247      * buf.put(floatArray, 0, floatArray.length);
248      *
249      * // Once we are done with a chunk of data, we need to
250      * // make sure all it written to the output
251      * buf.flush();
252      * </pre>
253      *
254      * @author Attila Kovacs
255      */
256     protected final class OutputBuffer {
257 
258         /**
259          * the byte array that stores pending data to be written to the output
260          */
261         private final byte[] data;
262 
263         /** the buffer wrapped for NIO access */
264         private final ByteBuffer buffer;
265 
266         /** The current type-specific view of the buffer or null */
267         private Buffer view;
268 
269         private OutputBuffer(int size) {
270             data = new byte[size];
271             buffer = ByteBuffer.wrap(data);
272         }
273 
274         /**
275          * Sets the byte order of the binary representation to which data is encoded.
276          *
277          * @param order the new byte order
278          *
279          * @see         #byteOrder()
280          * @see         ByteBuffer#order(ByteOrder)
281          */
282         protected void setByteOrder(ByteOrder order) {
283             buffer.order(order);
284         }
285 
286         /**
287          * Returns the current byte order of the binary representation to which data is encoded.
288          *
289          * @return the byte order
290          *
291          * @see    #setByteOrder(ByteOrder)
292          * @see    ByteBuffer#order()
293          */
294         protected ByteOrder byteOrder() {
295             return buffer.order();
296         }
297 
298         private boolean isViewingAs(Class<? extends Buffer> type) {
299             if (view == null) {
300                 return false;
301             }
302             return type.isAssignableFrom(view.getClass());
303         }
304 
305         private void assertView(ElementType<?> type) {
306             if (!isViewingAs(type.bufferClass())) {
307                 view = type.asTypedBuffer(buffer);
308             }
309         }
310 
311         private void rewind() {
312             buffer.rewind();
313             view = null;
314         }
315 
316         /**
317          * Puts a single byte into the conversion buffer, making space for it as needed by flushing the current buffer
318          * contents to the output as necessary.
319          *
320          * @param  b           the byte value
321          *
322          * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
323          *                         conversion.
324          *
325          * @see                #flush()
326          */
327         protected void putByte(byte b) throws IOException {
328             need(1);
329             view = null;
330             buffer.put(b);
331         }
332 
333         /**
334          * Puts a 2-byte integer into the conversion buffer, making space for it as needed by flushing the current
335          * buffer contents to the output as necessary.
336          *
337          * @param  s           the 16-bit integer value
338          *
339          * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
340          *                         conversion.
341          *
342          * @see                #flush()
343          */
344         protected void putShort(short s) throws IOException {
345             need(Short.BYTES);
346             view = null;
347             buffer.putShort(s);
348         }
349 
350         /**
351          * Puts a 4-byte integer into the conversion buffer, making space for it as needed by flushing the current
352          * buffer contents to the output as necessary.
353          *
354          * @param  i           the 32-bit integer value
355          *
356          * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
357          *                         conversion.
358          *
359          * @see                #flush()
360          */
361         protected void putInt(int i) throws IOException {
362             need(Integer.BYTES);
363             view = null;
364             buffer.putInt(i);
365         }
366 
367         /**
368          * Puts an 8-byte integer into the conversion buffer, making space for it as needed by flushing the current
369          * buffer contents to the output as necessary.
370          *
371          * @param  l           the 64-bit integer value
372          *
373          * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
374          *                         conversion.
375          *
376          * @see                #flush()
377          */
378         protected void putLong(long l) throws IOException {
379             need(Long.BYTES);
380             view = null;
381             buffer.putLong(l);
382         }
383 
384         /**
385          * Puts a 4-byte single-precision floating point value into the conversion buffer, making space for it as needed
386          * by flushing the current buffer contents to the output as necessary.
387          *
388          * @param  f           the 32-bit single-precision floating point value
389          *
390          * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
391          *                         conversion.
392          *
393          * @see                #flush()
394          */
395         protected void putFloat(float f) throws IOException {
396             need(Float.BYTES);
397             view = null;
398             buffer.putFloat(f);
399         }
400 
401         /**
402          * Puts an 8-byte double-precision floating point value into the conversion buffer, making space for it as
403          * needed by flushing the current buffer contents to the output as necessary.
404          *
405          * @param  d           the 64-bit double-precision floating point value
406          *
407          * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
408          *                         conversion.
409          *
410          * @see                #flush()
411          */
412         protected void putDouble(double d) throws IOException {
413             need(Double.BYTES);
414             view = null;
415             buffer.putDouble(d);
416         }
417 
418         /**
419          * Puts an array of bytes into the conversion buffer, flushing the buffer intermittently as necessary to make
420          * room as it goes.
421          *
422          * @param  src         an array of byte values
423          * @param  start       the index of the first element to convert
424          * @param  length      the number of elements to convert
425          *
426          * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
427          *                         conversion.
428          */
429         protected void put(byte[] src, int start, int length) throws IOException {
430             if (length == 1) {
431                 need(1);
432                 buffer.put(src[start]);
433                 return;
434             }
435 
436             view = null;
437 
438             int got = 0;
439 
440             while (got < length) {
441                 need(1);
442                 int m = Math.min(length - got, buffer.remaining());
443                 buffer.put(src, start + got, m);
444                 got += m;
445             }
446         }
447 
448         /**
449          * Puts an array of 16-bit integers into the conversion buffer, flushing the buffer intermittently as necessary
450          * to make room as it goes.
451          *
452          * @param  src         an array of 16-bit integer values
453          * @param  start       the index of the first element to convert
454          * @param  length      the number of elements to convert
455          *
456          * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
457          *                         conversion.
458          */
459         protected void put(short[] src, int start, int length) throws IOException {
460             if (length == 1 && !isViewingAs(ElementType.SHORT.bufferClass())) {
461                 putShort(src[start]);
462             } else {
463                 put(ElementType.SHORT, src, start, length);
464             }
465         }
466 
467         /**
468          * Puts an array of 32-bit integers into the conversion buffer, flushing the buffer intermittently as necessary
469          * to make room as it goes.
470          *
471          * @param  src         an array of 32-bit integer values
472          * @param  start       the index of the first element to convert
473          * @param  length      the number of elements to convert
474          *
475          * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
476          *                         conversion.
477          */
478         protected void put(int[] src, int start, int length) throws IOException {
479             if (length == 1 && !isViewingAs(ElementType.INT.bufferClass())) {
480                 putInt(src[start]);
481             } else {
482                 put(ElementType.INT, src, start, length);
483             }
484         }
485 
486         /**
487          * Puts an array of 64-bit integers into the conversion buffer, flushing the buffer intermittently as necessary
488          * to make room as it goes.
489          *
490          * @param  src         an array of 64-bit integer values
491          * @param  start       the index of the first element to convert
492          * @param  length      the number of elements to convert
493          *
494          * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
495          *                         conversion.
496          */
497         protected void put(long[] src, int start, int length) throws IOException {
498             if (length == 1 && !isViewingAs(ElementType.LONG.bufferClass())) {
499                 putLong(src[start]);
500             } else {
501                 put(ElementType.LONG, src, start, length);
502             }
503         }
504 
505         /**
506          * Puts an array of 32-bit single-precision floating point values into the conversion buffer, flushing the
507          * buffer intermittently as necessary to make room as it goes.
508          *
509          * @param  src         an array of 32-bit single-precision floating point values
510          * @param  start       the index of the first element to convert
511          * @param  length      the number of elements to convert
512          *
513          * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
514          *                         conversion.
515          */
516         protected void put(float[] src, int start, int length) throws IOException {
517             if (length == 1 && !isViewingAs(ElementType.FLOAT.bufferClass())) {
518                 putFloat(src[start]);
519             } else {
520                 put(ElementType.FLOAT, src, start, length);
521             }
522         }
523 
524         /**
525          * Puts an array of 64-bit double-precision floating point values into the conversion buffer, flushing the
526          * buffer intermittently as necessary to make room as it goes.
527          *
528          * @param  src         an array of 64-bit double-precision floating point values
529          * @param  start       the index of the first element to convert
530          * @param  length      the number of elements to convert
531          *
532          * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
533          *                         conversion.
534          */
535         protected void put(double[] src, int start, int length) throws IOException {
536             if (length == 1 && !isViewingAs(ElementType.DOUBLE.bufferClass())) {
537                 putDouble(src[start]);
538             } else {
539                 put(ElementType.DOUBLE, src, start, length);
540             }
541         }
542 
543         /**
544          * Puts an array of 64-bit values into the conversion buffer, flushing the buffer intermittently as necessary to
545          * make room as it goes.
546          *
547          * @param  e           FITS element type of the the 1D array
548          * @param  array       a 1D array of values of the specified element type
549          * @param  start       the index of the first element to convert
550          * @param  length      the number of elements to convert
551          *
552          * @throws IOException if the conversion buffer could not be flushed to the output to make room for the new
553          *                         conversion.
554          */
555         @SuppressWarnings("unchecked")
556         private <B extends Buffer> void put(ElementType<B> e, Object array, int start, int length)
557                 throws EOFException, IOException {
558             int got = 0;
559 
560             while (got < length) {
561                 need(e.size());
562                 assertView(e);
563                 int m = Math.min(length - got, view.remaining());
564                 e.putArray((B) view, array, start + got, m);
565                 buffer.position(buffer.position() + m * e.size());
566                 got += m;
567             }
568         }
569     }
570 }