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 }