All Downloads are FREE. Search and download functionalities are using the official Maven repository.

nom.tam.util.InputDecoder Maven / Gradle / Ivy

/*
 * #%L
 * nom.tam FITS library
 * %%
 * Copyright (C) 1996 - 2024 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.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

import nom.tam.fits.FitsFactory;
import nom.tam.util.type.ElementType;

/**
 * Efficient base class for decoding of binary input into Java arrays (primarily for internal use)
 *
 * @author Attila Kovacs
 *
 * @since  1.16
 *
 * @see    OutputEncoder
 * @see    ArrayDataFile
 * @see    ArrayInputStream
 * @see    ArrayOutputStream
 */
public abstract class InputDecoder {

    /** The buffer size for array translation */
    private static final int BUFFER_SIZE = 8 * FitsFactory.FITS_BLOCK_SIZE;

    /** bit mask for 1 byte */
    private static final int BYTE_MASK = 0xFF;

    /** bit mask for a 16-byte integer (a Java short). */
    private static final int SHORT_MASK = 0xFFFF;

    /** the input providing the binary representation of data */
    private InputReader in;

    /** the conversion buffer */
    private InputBuffer buf;

    /**
     * Instantiates a new decoder of binary input to Java arrays. To be used by subclass constructors only.
     *
     * @see #setInput(InputReader)
     */
    protected InputDecoder() {
        buf = new InputBuffer(BUFFER_SIZE);
    }

    /**
     * Instantiates a new decoder for converting data representations into Java arrays.
     *
     * @param i the binary input.
     */
    public InputDecoder(InputReader i) {
        this();
        setInput(i);
    }

    /**
     * Sets the input from which to read the binary output.
     *
     * @param i the new binary input.
     */
    protected void setInput(InputReader i) {
        in = i;
    }

    /**
     * Returns the buffer that is used for conversion, which can be used to bulk read bytes ahead from the input (see
     * {@link InputBuffer#loadBytes(long, int)}) and {@link InputBuffer#loadOne(int)}) before doing conversions to Java
     * types locally.
     *
     * @return the conversion buffer used by this decoder.
     */
    protected InputBuffer getInputBuffer() {
        return buf;
    }

    /**
     * Makes sure that an elements of the specified size is fully available in the buffer, prompting additional reading
     * of the underlying stream as appropriate (but not beyond the limit set by {@link #loadBytes(long, int)}.
     *
     * @param  size        the number of bytes we need at once from the buffer
     *
     * @return             true if the requested number of bytes are, or could be made, available.
     *                         Otherwise false.
     *
     * @throws IOException if there was an underlying IO error, other than the end of file, while trying to fetch
     *                         additional data from the underlying input
     */
    boolean makeAvailable(int size) throws IOException {
        // TODO Once the deprecated BufferDecoder is retired, this should become
        // a private method of InputBuffer (with buf. prefixed removed below).
        while (buf.buffer.remaining() < size) {
            if (!buf.fetch()) {
                return false;
            }
        }
        return true;
    }

    /**
     * Reads one byte from the input. See the contract of {@link InputStream#read()}.
     *
     * @return             the byte, or -1 if at the end of the file.
     *
     * @throws IOException if an IO error, other than the end-of-file prevented the read.
     */
    protected synchronized int read() throws IOException {
        return in.read();
    }

    /**
     * Reads bytes into an array from the input. See the contract of {@link InputStream#read(byte[], int, int)}.
     *
     * @param  b           the destination array
     * @param  start       the first index in the array to be populated
     * @param  length      the number of bytes to read into the array.
     *
     * @return             the number of bytes successfully read, or -1 if at the end of the file.
     *
     * @throws IOException if an IO error, other than the end-of-file prevented the read.
     */
    protected synchronized int read(byte[] b, int start, int length) throws IOException {
        return in.read(b, start, length);
    }

    /**
     * Reads bytes to fill the supplied buffer with the requested number of bytes from the given starting buffer index.
     * If not enough bytes are avaialable in the file to deliver the reqauested number of bytes the buffer, an
     * {@link EOFException} will be thrown.
     *
     * @param  b            the buffer
     * @param  off          the buffer index at which to start reading data
     * @param  len          the total number of bytes to read.
     *
     * @throws EOFException if already at the end of file.
     * @throws IOException  if there was an IO error before the requested number of bytes could all be read.
     */
    protected void readFully(byte[] b, int off, int len) throws EOFException, IOException {
        while (len > 0) {
            int n = read(b, off, len);
            if (n < 0) {
                throw new EOFException();
            }
            off += n;
            len -= n;
        }
    }

    /**
     * Based on {@link #readArray(Object)}, but guaranteeing a complete read of the supplied object or else an
     * {@link EOFException} is thrown.
     *
     * @param  o                        the array, including multi-dimensional, and heterogeneous arrays of arrays.
     *
     * @throws EOFException             if already at the end of file.
     * @throws IOException              if there was an IO error
     * @throws IllegalArgumentException if the argument is not a Java array, or is or contains elements that do not have
     *                                      supported conversions from binary representation.
     *
     * @see                             #readArray(Object)
     * @see                             #readImage(Object)
     */
    public void readArrayFully(Object o) throws IOException, IllegalArgumentException {
        if (readArray(o) != FitsEncoder.computeSize(o)) {
            throw new EOFException("Incomplete array read (FITS encoding).");
        }
    }

    /**
     * See the contract of {@link ArrayDataInput#readLArray(Object)}.
     *
     * @param  o                        an array, to be populated
     *
     * @return                          the actual number of bytes read from the input, or -1 if already at the
     *                                      end-of-file.
     *
     * @throws IllegalArgumentException if the argument is not an array or if it contains an element that is not
     *                                      supported for decoding.
     * @throws IOException              if there was an IO error reading from the input
     *
     * @see                             #readArrayFully(Object)
     */
    public abstract long readArray(Object o) throws IOException, IllegalArgumentException;

    /**
     * Like {@link #readArrayFully(Object)} but strictly for numerical types only.
     *
     * @param  o                        An any-dimensional array containing only numerical types
     *
     * @throws IllegalArgumentException if the argument is not an array or if it contains an element that is not
     *                                      supported.
     * @throws EOFException             if already at the end of file.
     * @throws IOException              if there was an IO error
     *
     * @see                             #readArrayFully(Object)
     *
     * @since                           1.18
     */
    public void readImage(Object o) throws IOException, IllegalArgumentException {
        if (o == null) {
            return;
        }

        if (!o.getClass().isArray()) {
            throw new IllegalArgumentException("Not an array: " + o.getClass().getName());
        }

        long size = FitsEncoder.computeSize(o);
        if (size == 0) {
            return;
        }

        getInputBuffer().loadBytes(size, 1);
        if (getImage(o) != size) {
            throw new EOFException("Incomplete image read.");
        }
    }

    private long getImage(Object o) throws IOException, IllegalArgumentException {
        int length = Array.getLength(o);
        if (length == 0) {
            return 0L;
        }

        if (o instanceof byte[]) {
            return buf.get((byte[]) o, 0, length);
        }
        if (o instanceof short[]) {
            return buf.get((short[]) o, 0, length) * Short.BYTES;
        }
        if (o instanceof int[]) {
            return buf.get((int[]) o, 0, length) * Integer.BYTES;
        }
        if (o instanceof float[]) {
            return buf.get((float[]) o, 0, length) * Float.BYTES;
        }
        if (o instanceof long[]) {
            return buf.get((long[]) o, 0, length) * Long.BYTES;
        }
        if (o instanceof double[]) {
            return buf.get((double[]) o, 0, length) * Double.BYTES;
        }
        if (!(o instanceof Object[])) {
            throw new IllegalArgumentException("Not a numerical image type: " + o.getClass().getName());
        }

        Object[] array = (Object[]) o;
        long count = 0L;

        // Process multidim arrays recursively.
        for (int i = 0; i < length; i++) {
            try {
                count += getImage(array[i]);
            } catch (EOFException e) {
                return eofCheck(e, count, -1L);
            }
        }
        return count;
    }

    /**
     * Decides what to do when an {@link EOFException} is encountered after having read some number of bytes from the
     * input. The default behavior is to re-throw the exception only if no data at all was obtained from the input,
     * otherwise return the non-zero byte count of data that were successfully read. Subclass implementations may
     * override this method to adjust if an when {@link EOFException} is thrown upon an incomplete read.
     *
     * @param  e            the exception that was thrown, or null.
     * @param  got          the number of elements successfully read
     * @param  expected     the number of elements expected
     *
     * @return              the number of elements successfully read (same as got).
     *
     * @throws EOFException the rethrown exception, or a new one, as appropriate
     */
    long eofCheck(EOFException e, long got, long expected) throws EOFException {
        if (got == 0) {
            if (e == null) {
                throw new EOFException();
            }
            throw e;
        }
        return got;
    }

    /**
     * 

* The conversion buffer for decoding binary data representation into Java arrays (objects). *

*

* The buffering is most efficient, if we fist specify how many bytes of input maybe be consumed first (buffered * from the input), via {@link #loadBytes(long, int)}. After that, we can call the get routines of this class to * return binary data converted to Java format until we exhaust the specified alotment of bytes. *

* *
     * // The data we want to retrieve
     * double d;
     * int i;
     * short[] shortArray = new short[100];
     * float[] floaTarray = new float[48];
     *
     * // We convert from the binary format to Java format using
     * // the local conversion buffer
     * ConversionBuffer buf = getBuffer();
     *
     * // We can allow the conversion buffer to read enough bytes for all
     * // data we want to retrieve:
     * buf.loadBytes(FitsIO.BYTES_IN_DOUBLE + FitsIO.BYTES_IN_INT + FitsIO.BYTES_IN_SHORT * shortArray.length
     *         + FitsIO.BYTES_IN_FLOAT * floatArray.length);
     *
     * // Now we can get the data with minimal underlying IO calls...
     * d = buf.getDouble();
     * i = buf.getInt();
     *
     * for (int i = 0; i < shortArray.length; i++) {
     *     shortArray[i] = buf.getShort();
     * }
     *
     * for (int i = 0; i < floatArray.length; i++) {
     *     floatArray[i] = buf.getFloat();
     * }
     * 
*

* In the special case that one needs just a single element (or a few single elements) from the input, rather than * lots of elements or arrays, one may use {@link #loadOne(int)} instead of {@link #loadBytes(long, int)} to read * just enough bytes for a single data element from the input before each conversion. For example: *

* *
     * ConversionBuffer buf = getBuffer();
     *
     * buf.loadOne(FitsIO.BYTES_IN_FLOAT);
     * float f = buf.getFloat();
     * 
* * @author Attila Kovacs */ protected final class InputBuffer { /** the byte array in which to buffer data from the input */ private final byte[] data; /** the buffer wrapped for NIO access */ private final ByteBuffer buffer; /** The current type-specific view of the buffer or null */ private Buffer view; /** the number of bytes requested, but not yet buffered */ private long pending = 0; private InputBuffer(int size) { data = new byte[size]; buffer = ByteBuffer.wrap(data); } /** * Sets the byte order of the binary data representation from which we are decoding data. * * @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 data representation from which we are decoding. * * @return the byte order * * @see #setByteOrder(ByteOrder) * @see ByteBuffer#order() */ protected ByteOrder byteOrder() { return buffer.order(); } private boolean isViewingAs(Class type) { if (view == null) { return false; } return type.isAssignableFrom(view.getClass()); } private void assertView(ElementType type) { if (!isViewingAs(type.bufferClass())) { view = type.asTypedBuffer(buffer); } } private void rewind() { buffer.rewind(); view = null; } /** * Set the number of bytes we can buffer from the input for subsequent retrieval from this buffer. The get * methods of this class will be ensured not to fetch data from the input beyond the requested size. * * @param n the number of elements we can read and buffer from the input * @param size the number of bytes in each elements. */ protected void loadBytes(long n, int size) { rewind(); buffer.limit(0); pending = n * size; } /** * Loads just a single element of the specified byte size. The element must fit into the conversion buffer, and * it is up to the caller to ensure that. The method itself does not check. * * @param size The number of bytes in the element * * @return true if the data was successfully read from the uderlying stream or file, * otherwise false. * * @throws IOException if there was an IO error, other than the end-of-file. */ protected boolean loadOne(int size) throws IOException { pending = size; rewind(); buffer.limit(0); return makeAvailable(size); } /** * Reads more data into the buffer from the underlying stream, attempting to fill the buffer if possible. * * @return true if data was successfully buffered from the underlying intput or the * buffer is already full. Otherwise false. * * @throws IOException if there as an IO error, other than the end of file, while trying to read more data from * the underlying input into the buffer. */ private boolean fetch() throws IOException { int remaining = buffer.remaining(); if (remaining > 0) { System.arraycopy(data, buffer.position(), data, 0, remaining); } rewind(); int n = (int) Math.min(pending, data.length - remaining); n = in.read(data, remaining, n); if (n < 0) { return false; } buffer.limit(remaining + n); pending -= n; return true; } /** * Retrieves a single byte from the buffer. Before data can be retrieved with this method they should be * 'loaded' into the buffer via {@link #loadOne(int)} or {@link #loadBytes(long, int)}. This method is * appropriate for retrieving one or a fwew bytes at a time. For bulk input of bytes, you should use * {@link InputDecoder#read(byte[], int, int)} instead for superior performance. * * @return the byte value, or -1 if no more data is available from the buffer or the underlying * input. * * @throws IOException if there as an IO error, other than the end of file, while trying to read more data from * the underlying input into the buffer. * * @see #loadOne(int) * @see #loadBytes(long, int) */ protected int get() throws IOException { if (makeAvailable(1)) { view = null; return buffer.get() & BYTE_MASK; } return -1; } /** * Retrieves a 2-byte unsigned integer from the buffer. Before data can be retrieved with this method the should * be 'loaded' into the buffer via {@link #loadOne(int)} or {@link #loadBytes(long, int)}. * * @return the 16-bit integer value, or -1 if no more data is available from the buffer or the * underlying input. * * @throws IOException if there as an IO error, other than the end of file, while trying to read more data from * the underlying input into the buffer. * * @see #loadOne(int) * @see #loadBytes(long, int) */ protected int getUnsignedShort() throws IOException { if (makeAvailable(Short.BYTES)) { view = null; return buffer.getShort() & SHORT_MASK; } return -1; } /** * Retrieves a 4-byte integer from the buffer. Before data can be retrieved with this method the should be * 'loaded' into the buffer via {@link #loadOne(int)} or {@link #loadBytes(long, int)}. * * @return the 32-bit integer value. * * @throws EOFException if already at the end of file. * @throws IOException if there as an IO error * * @see #loadOne(int) * @see #loadBytes(long, int) */ protected int getInt() throws EOFException, IOException { if (makeAvailable(Integer.BYTES)) { view = null; return buffer.getInt(); } throw new EOFException(); } /** * Retrieves a 8-byte integer from the buffer. Before data can be retrieved with this method the should be * 'loaded' into the buffer via {@link #loadOne(int)} or {@link #loadBytes(long, int)}. * * @return the 64-bit integer value. * * @throws EOFException if already at the end of file. * @throws IOException if there as an IO error * * @see #loadOne(int) * @see #loadBytes(long, int) */ protected long getLong() throws EOFException, IOException { if (makeAvailable(Long.BYTES)) { view = null; return buffer.getLong(); } throw new EOFException(); } /** * Retrieves a 4-byte single-precision floating point value from the buffer. Before data can be retrieved with * this method the shold be 'loaded' into the buffer via {@link #loadOne(int)} or {@link #loadBytes(long, int)}. * * @return the 32-bit single-precision floating-point value. * * @throws EOFException if already at the end of file. * @throws IOException if there as an IO error * * @see #loadOne(int) * @see #loadBytes(long, int) */ protected float getFloat() throws EOFException, IOException { if (makeAvailable(Float.BYTES)) { view = null; return buffer.getFloat(); } throw new EOFException(); } /** * Retrieves a 8-byte double-precision floating point value from the buffer. Before data can be retrieved with * this method they should be 'loaded' into the buffer via {@link #loadOne(int)} or * {@link #loadBytes(long, int)}. * * @return the 64-bit double-precision floating-point value. * * @throws EOFException if already at the end of file. * @throws IOException if there as an IO error * * @see #loadOne(int) * @see #loadBytes(long, int) */ protected double getDouble() throws EOFException, IOException { if (makeAvailable(Double.BYTES)) { view = null; return buffer.getDouble(); } throw new EOFException(); } /** * Retrieves a sequence of signed bytes from the buffer. Before data can be retrieved with this method the * should be 'loaded' into the buffer via {@link #loadBytes(long, int)}. * * @param dst Java array in which to store the retrieved sequence of elements * @param from the array index for storing the first element retrieved * @param n the number of elements to retrieve * * @return the number of elements successfully retrieved * * @throws EOFException if already at the end of file. * @throws IOException if there was an IO error before, before requested number of bytes could be read * * @see #loadBytes(long, int) * * @since 1.18 */ protected int get(byte[] dst, int from, int n) throws EOFException, IOException { if (n == 1) { int i = get(); if (i < 0) { throw new EOFException(); } dst[from] = (byte) i; return 1; } view = null; int got = 0; while (got < n) { if (!makeAvailable(1)) { return (int) eofCheck(null, got, n); } int m = Math.min(n - got, buffer.remaining()); buffer.get(dst, from + got, m); got += m; } return got; } /** * Retrieves a sequence of big-endian 16-bit signed integers from the buffer. Before data can be retrieved with * this method they should be 'loaded' into the buffer via {@link #loadBytes(long, int)}. * * @param dst Java array in which to store the retrieved sequence of elements * @param from the array index for storing the first element retrieved * @param n the number of elements to retrieve * * @return the number of elements successfully retrieved * * @throws EOFException if already at the end of file. * @throws IOException if there was an IO error before, before requested number of bytes could be read * * @see #loadBytes(long, int) * * @since 1.18 */ protected int get(short[] dst, int from, int n) throws EOFException, IOException { if (n == 1 && !isViewingAs(ElementType.SHORT.bufferClass())) { int i = getUnsignedShort(); if (i < 0) { throw new EOFException(); } dst[from] = (short) i; return 1; } return get(ElementType.SHORT, dst, from, n); } /** * Retrieves a sequence of big-endian 32-bit signed integers from the buffer. Before data can be retrieved with * this method they should be 'loaded' into the buffer via {@link #loadBytes(long, int)}. * * @param dst Java array in which to store the retrieved sequence of elements * @param from the array index for storing the first element retrieved * @param n the number of elements to retrieve * * @return the number of elements successfully retrieved * * @throws EOFException if already at the end of file. * @throws IOException if there was an IO error before, before requested number of bytes could be read * * @see #loadBytes(long, int) * * @since 1.18 */ protected int get(int[] dst, int from, int n) throws EOFException, IOException { if (n == 1 && !isViewingAs(ElementType.INT.bufferClass())) { dst[from] = getInt(); return 1; } return get(ElementType.INT, dst, from, n); } /** * Retrieves a sequence of big-endian 64-bit signed integers from the buffer. Before data can be retrieved with * this method they should be 'loaded' into the buffer via {@link #loadBytes(long, int)}. * * @param dst Java array in which to store the retrieved sequence of elements * @param from the array index for storing the first element retrieved * @param n the number of elements to retrieve * * @return the number of elements successfully retrieved * * @throws EOFException if already at the end of file. * @throws IOException if there was an IO error before, before requested number of bytes could be read * * @see #loadBytes(long, int) * * @since 1.18 */ protected int get(long[] dst, int from, int n) throws EOFException, IOException { if (n == 1 && !isViewingAs(ElementType.LONG.bufferClass())) { dst[from] = getLong(); return 1; } return get(ElementType.LONG, dst, from, n); } /** * Retrieves a sequence of big-endian 32-bit floating-point values from the buffer. Before data can be retrieved * with this method they should be 'loaded' into the buffer via {@link #loadBytes(long, int)}. * * @param dst Java array in which to store the retrieved sequence of elements * @param from the array index for storing the first element retrieved * @param n the number of elements to retrieve * * @return the number of elements successfully retrieved * * @throws EOFException if already at the end of file. * @throws IOException if there was an IO error before, before requested number of bytes could be read * * @see #loadBytes(long, int) * * @since 1.18 */ protected int get(float[] dst, int from, int n) throws EOFException, IOException { if (n == 1 && !isViewingAs(ElementType.FLOAT.bufferClass())) { dst[from] = getFloat(); return 1; } return get(ElementType.FLOAT, dst, from, n); } /** * Retrieves a sequence of big-endian 64-bit floating-point values from the buffer. Before data can be retrieved * with this method they should be 'loaded' into the buffer via {@link #loadBytes(long, int)}. * * @param dst Java array in which to store the retrieved sequence of elements * @param from the array index for storing the first element retrieved * @param n the number of elements to retrieve * * @return the number of elements successfully retrieved * * @throws EOFException if already at the end of file. * @throws IOException if there was an IO error before, before requested number of bytes could be read * * @see #loadBytes(long, int) * * @since 1.18 */ protected int get(double[] dst, int from, int n) throws EOFException, IOException { if (n == 1 && !isViewingAs(ElementType.DOUBLE.bufferClass())) { dst[from] = getDouble(); return 1; } return get(ElementType.DOUBLE, dst, from, n); } /** * Retrieves a sequence of values from the buffer. Before data can be retrieved with this method the should be * 'loaded' into the buffer via {@link #loadBytes(long, int)}. * * @param dst Java array in which to store the retrieved sequence of elements * @param from the array index for storing the first element retrieved * @param n the number of elements to retrieve * * @return the number of elements successfully retrieved * * @throws EOFException if already at the end of file. * @throws IOException if there was an IO error before, before requested number of bytes could be read * * @see #loadBytes(long, int) * * @since 1.18 */ @SuppressWarnings("unchecked") private int get(ElementType e, Object dst, int from, int n) throws EOFException, IOException { int got = 0; while (got < n) { if (!makeAvailable(e.size())) { return (int) eofCheck(null, got, n); } assertView(e); int m = Math.min(n - got, view.remaining()); e.getArray((B) view, dst, from + got, m); buffer.position(buffer.position() + m * e.size()); got += m; } return got; } } }