ArrayEncoder.java

/*
 * #%L
 * nom.tam FITS library
 * %%
 * Copyright (C) 1996 - 2021 nom-tam-fits
 * %%
 * This is free and unencumbered software released into the public domain.
 * 
 * Anyone is free to copy, modify, publish, use, compile, sell, or
 * distribute this software, either in source code form or as a compiled
 * binary, for any purpose, commercial or non-commercial, and by any
 * means.
 * 
 * In jurisdictions that recognize copyright laws, the author or authors
 * of this software dedicate any and all copyright interest in the
 * software to the public domain. We make this dedication for the benefit
 * of the public at large and to the detriment of our heirs and
 * successors. We intend this dedication to be an overt act of
 * relinquishment in perpetuity of all present and future rights to this
 * software under copyright law.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 * #L%
 */

package nom.tam.util;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

import nom.tam.fits.FitsFactory;

/**
 * Efficient base class for encoding Java arrays into binary output.
 * 
 * @author Attila Kovacs
 * @since 1.16
 * @see ArrayDecoder
 * @see ArrayDataFile
 * @see ArrayInputStream
 * @see ArrayOutputStream
 */
public abstract class ArrayEncoder {

    /**
     * The default local buffer size to use for encoding data into binary format
     */
    private static final int BUFFER_SIZE = FitsFactory.FITS_BLOCK_SIZE;

    /**
     * The output to which to write encoded data (directly or from the
     * conversion buffer)
     */
    private OutputWriter out;

    /**
     * A local buffer for efficient conversions before bulk writing to the
     * output
     */
    private OutputBuffer buf;

    /**
     * Instantiates a new Java-to-binary encoder for arrays. To be used by
     * subclass implementations only
     * 
     * @see #setOutput(OutputWriter)
     */
    protected ArrayEncoder() {
        buf = new OutputBuffer(BUFFER_SIZE);
    }

    /**
     * Instantiates a new Java-to-binary encoder for arrays, writing encoded
     * data to the specified output.
     * 
     * @param o
     *            the output to which encoded data is to be written.
     */
    public ArrayEncoder(OutputWriter o) {
        this();
        setOutput(o);
    }

    /**
     * Sets the output to which encoded data should be written (directly or from
     * the conversion buffer).
     * 
     * @param o
     *            the new output to which encoded data is to be written.
     */
    protected synchronized void setOutput(OutputWriter o) {
        this.out = o;
    }

    /**
     * Returns the buffer that is used for conversion, which can be used to
     * collate more elements for writing before bulk flushing data to the output
     * (see {@link OutputBuffer#flush()}).
     * 
     * @return the conversion buffer used by this encoder.
     */
    protected OutputBuffer getOutputBuffer() {
        return buf;
    }

    /**
     * Makes sure that there is room in the conversion buffer for an upcoming
     * element conversion, and flushes the buffer as necessary to make room.
     * Subclass implementations should call this method before attempting a
     * conversion operation.
     * 
     * @param bytes
     *            the size of an element we will want to convert. It cannot
     *            exceed the size of the conversion buffer.
     * @throws IOException
     *             if the conversion buffer could not be flushed to the output
     *             to make room for the new conversion.
     */
    void need(int bytes) throws IOException {
        // TODO Once the deprecated {@link BufferEncoder} is retired, this
        // should become
        // a private method of OutputBuffer, with leading 'buf.' references
        // stripped.
        if (buf.buffer.remaining() < bytes) {
            flush();
        }
    }

    /**
     * Flushes the contents of the conversion buffer to the underlying output.
     * 
     * @throws IOException
     *             if there was an IO error writing the contents of this buffer
     *             to the output.
     */
    protected synchronized void flush() throws IOException {
        int n = buf.buffer.position();
        out.write(buf.data, 0, n);
        buf.buffer.rewind();
    }

    /**
     * Writes a byte. See the general contract of
     * {@link java.io.DataOutputStream#write(int)}.
     * 
     * @param b
     *            the (unsigned) byte value to write.
     * @throws IOException
     *             if there was an underlying IO error
     * @see java.io.DataOutputStream#write(int)
     */
    protected synchronized void write(int b) throws IOException {
        flush();
        out.write(b);
    }

    /**
     * Writes up to the specified number of bytes from a buffer to the stream.
     * See the general contract of
     * {@link java.io.DataOutputStream#write(byte[], int, int)}.
     * 
     * @param b
     *            the buffer
     * @param start
     *            the starting buffer index
     * @param length
     *            the number of bytes to write.
     * @throws IOException
     *             if there was an underlying IO error
     * @see java.io.DataOutputStream#write(byte[], int, int)
     */
    protected synchronized void write(byte[] b, int start, int length) throws IOException {
        flush();
        out.write(b, start, length);
    }

    /**
     * Writes the contents of a Java array to the output translating the data to
     * the required binary representation. The argument may be any generic Java
     * array, including heterogeneous arrays of arrays.
     * 
     * @param o
     *            the Java array, including heterogeneous arrays of arrays. If
     *            <code>null</code> nothing will be written to the output.
     * @throws IOException
     *             if there was an IO error writing to the output
     * @throws IllegalArgumentException
     *             if the supplied object is not a Java array or if it contains
     *             Java types that are not supported by the decoder.
     * @see ArrayDataOutput#writeArray(Object)
     */
    public abstract void writeArray(Object o) throws IOException, IllegalArgumentException;

    /**
     * <p>
     * The conversion buffer for encoding Java arrays (objects) into a binary
     * data representation.
     * </p>
     * <p>
     * The buffering is most efficient if multiple conversions (put methods) are
     * collated before a forced {@link #flush()} call to the output. The caller
     * need not worry about space remaining in the buffer. As new data is placed
     * (put) into the buffer, the buffer will automatically flush the contents
     * to the output to make space for new elements as it goes. The caller only
     * needs to call the final {@link #flush()}, to ensure that all elements
     * bufferes so far are written to the output.
     * </p>
     * 
     * <pre>
     * short[] shortArray = new short[100];
     * float[] floaTarray = new float[48];
     * 
     * // populate the arrays with data...
     * 
     * // Convert to binary representation using the local
     * // conversion buffer.
     * ConversionBuffer buf = getBuffer();
     * 
     * // Convert as much data as we want to the output format...
     * buf.putDouble(1.0);
     * buf.putInt(-1);
     * buf.put(shortArray, 0, shortArray.length);
     * buf.put(floatArray, 0, floatArray.length);
     * 
     * // Once we are done with a chunk of data, we need to
     * // make sure all it written to the output
     * buf.flush();
     * </pre>
     * 
     * @author Attila Kovacs
     */
    protected final class OutputBuffer {

        /** the byte array that stores pending data to be written to the output */
        private final byte[] data;

        /** the buffer wrapped for NIO access */
        private final ByteBuffer buffer;

        private OutputBuffer(int size) {
            this.data = new byte[size];
            buffer = ByteBuffer.wrap(data);
        }

        /**
         * Sets the byte order of the binary representation to which data is
         * encoded.
         * 
         * @param order
         *            the new byte order
         * @see #byteOrder()
         * @see ByteBuffer#order(ByteOrder)
         */
        protected void setByteOrder(ByteOrder order) {
            buffer.order(order);
        }

        /**
         * Returns the current byte order of the binary representation to which
         * data is encoded.
         * 
         * @return the byte order
         * @see #setByteOrder(ByteOrder)
         * @see ByteBuffer#order()
         */
        protected ByteOrder byteOrder() {
            return buffer.order();
        }

        /**
         * Puts a single byte into the conversion buffer, making space for it as
         * needed by flushing the current buffer contents to the output as
         * necessary.
         * 
         * @param b
         *            the byte value
         * @throws IOException
         *             if the conversion buffer could not be flushed to the
         *             output to make room for the new conversion.
         * @see #flush()
         */
        protected void putByte(byte b) throws IOException {
            need(1);
            buffer.put(b);
        }

        /**
         * Puts a 2-byte integer into the conversion buffer, making space for it
         * as needed by flushing the current buffer contents to the output as
         * necessary.
         * 
         * @param s
         *            the 16-bit integer value
         * @throws IOException
         *             if the conversion buffer could not be flushed to the
         *             output to make room for the new conversion.
         * @see #flush()
         */
        protected void putShort(short s) throws IOException {
            need(FitsIO.BYTES_IN_SHORT);
            buffer.putShort(s);
        }

        /**
         * Puts a 4-byte integer into the conversion buffer, making space for it
         * as needed by flushing the current buffer contents to the output as
         * necessary.
         * 
         * @param i
         *            the 32-bit integer value
         * @throws IOException
         *             if the conversion buffer could not be flushed to the
         *             output to make room for the new conversion.
         * @see #flush()
         */
        protected void putInt(int i) throws IOException {
            need(FitsIO.BYTES_IN_INTEGER);
            buffer.putInt(i);
        }

        /**
         * Puts an 8-byte integer into the conversion buffer, making space for
         * it as needed by flushing the current buffer contents to the output as
         * necessary.
         * 
         * @param l
         *            the 64-bit integer value
         * @throws IOException
         *             if the conversion buffer could not be flushed to the
         *             output to make room for the new conversion.
         * @see #flush()
         */
        protected void putLong(long l) throws IOException {
            need(FitsIO.BYTES_IN_LONG);
            buffer.putLong(l);
        }

        /**
         * Puts an 4-byte single-precision floating point value into the
         * conversion buffer, making space for it as needed by flushing the
         * current buffer contents to the output as necessary.
         * 
         * @param f
         *            the 32-bit single-precision floating point value
         * @throws IOException
         *             if the conversion buffer could not be flushed to the
         *             output to make room for the new conversion.
         * @see #flush()
         */
        protected void putFloat(float f) throws IOException {
            need(FitsIO.BYTES_IN_FLOAT);
            buffer.putFloat(f);
        }

        /**
         * Puts an 8-byte double-precision floating point value into the
         * conversion buffer, making space for it as needed by flushing the
         * current buffer contents to the output as necessary.
         * 
         * @param d
         *            the 64-bit double-precision floating point value
         * @throws IOException
         *             if the conversion buffer could not be flushed to the
         *             output to make room for the new conversion.
         * @see #flush()
         */
        protected void putDouble(double d) throws IOException {
            need(FitsIO.BYTES_IN_DOUBLE);
            buffer.putDouble(d);
        }

        /**
         * Puts an array of 16-bit integers into the conversion buffer, flushing
         * the buffer intermittently as necessary to make room as it goes.
         * 
         * @param s
         *            an array of 16-bit integer values
         * @param start
         *            the index of the first element to convert
         * @param length
         *            the number of elements to convert
         * @throws IOException
         *             if the conversion buffer could not be flushed to the
         *             output to make room for the new conversion.
         */
        protected void put(short[] s, int start, int length) throws IOException {
            length += start;
            while (start < length) {
                putShort(s[start++]);
            }
        }

        /**
         * Puts an array of 32-bit integers into the conversion buffer, flushing
         * the buffer intermittently as necessary to make room as it goes.
         * 
         * @param i
         *            an array of 32-bit integer values
         * @param start
         *            the index of the first element to convert
         * @param length
         *            the number of elements to convert
         * @throws IOException
         *             if the conversion buffer could not be flushed to the
         *             output to make room for the new conversion.
         */
        protected void put(int[] i, int start, int length) throws IOException {
            length += start;
            while (start < length) {
                putInt(i[start++]);
            }
        }

        /**
         * Puts an array of 64-bit integers into the conversion buffer, flushing
         * the buffer intermittently as necessary to make room as it goes.
         * 
         * @param l
         *            an array of 64-bit integer values
         * @param start
         *            the index of the first element to convert
         * @param length
         *            the number of elements to convert
         * @throws IOException
         *             if the conversion buffer could not be flushed to the
         *             output to make room for the new conversion.
         */
        protected void put(long[] l, int start, int length) throws IOException {
            length += start;
            while (start < length) {
                putLong(l[start++]);
            }
        }

        /**
         * Puts an array of 32-bit single-precision floating point values into
         * the conversion buffer, flushing the buffer intermittently as
         * necessary to make room as it goes.
         * 
         * @param f
         *            an array of 32-bit single-precision floating point values
         * @param start
         *            the index of the first element to convert
         * @param length
         *            the number of elements to convert
         * @throws IOException
         *             if the conversion buffer could not be flushed to the
         *             output to make room for the new conversion.
         */
        protected void put(float[] f, int start, int length) throws IOException {
            length += start;
            while (start < length) {
                putFloat(f[start++]);
            }
        }

        /**
         * Puts an array of 64-bit double-precision floating point values into
         * the conversion buffer, flushing the buffer intermittently as
         * necessary to make room as it goes.
         * 
         * @param d
         *            an array of 64-bit double-precision floating point values
         * @param start
         *            the index of the first element to convert
         * @param length
         *            the number of elements to convert
         * @throws IOException
         *             if the conversion buffer could not be flushed to the
         *             output to make room for the new conversion.
         */
        protected void put(double[] d, int start, int length) throws IOException {
            length += start;
            while (start < length) {
                putDouble(d[start++]);
            }
        }

    }
}