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 }