OceanusDataConverter.java

/*******************************************************************************
 * Oceanus: Java Utilities
 * Copyright 2012,2025 Tony Washer
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package net.sourceforge.joceanus.oceanus.convert;

import net.sourceforge.joceanus.oceanus.base.OceanusException;
import net.sourceforge.joceanus.oceanus.exc.OceanusDataException;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

/**
 * Data Conversion utility functions.
 */
public final class OceanusDataConverter {
    /**
     * Invalid hexadecimal length string.
     */
    private static final String ERROR_HEXLEN = "Invalid HexString Length: ";

    /**
     * Invalid hexadecimal error string.
     */
    private static final String ERROR_HEXDIGIT = "Non Hexadecimal Value: ";

    /**
     * Base64 Encoding array.
     */
    private static final char[] BASE64_ENCODE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray();

    /**
     * Base64 Decoding array.
     */
    private static final int[] BASE64_DECODE = new int[BASE64_ENCODE.length << 1];

    static {
        for (int i = 0; i < BASE64_ENCODE.length; i++) {
            BASE64_DECODE[BASE64_ENCODE[i]] = i;
        }
    }

    /**
     * Base64 triplet size.
     */
    private static final int BASE64_TRIPLE = 3;

    /**
     * Base64 padding character.
     */
    private static final char BASE64_PAD = '=';

    /**
     * Base64 shift 1.
     */
    private static final int BASE64_SHIFT1 = 2;

    /**
     * Base64 shift 2.
     */
    private static final int BASE64_SHIFT2 = 4;

    /**
     * Base64 shift 3.
     */
    private static final int BASE64_SHIFT3 = 6;

    /**
     * Hexadecimal Radix.
     */
    public static final int HEX_RADIX = 16;

    /**
     * Byte shift.
     */
    public static final int BYTE_SHIFT = Byte.SIZE;

    /**
     * Byte mask.
     */
    public static final int BYTE_MASK = 0xFF;

    /**
     * Base64 mask.
     */
    public static final int BASE64_MASK = 0x3F;

    /**
     * Color mask.
     */
    public static final int COLOR_MASK = 0x00FFFFFF;

    /**
     * Nybble shift.
     */
    public static final int NYBBLE_SHIFT = Byte.SIZE >> 1;

    /**
     * Nybble mask.
     */
    public static final int NYBBLE_MASK = 0xF;

    /**
     * RGB colour length.
     */
    public static final int RGB_LENGTH = 6;

    /**
     * Private constructor to avoid instantiation.
     */
    private OceanusDataConverter() {
    }

    /**
     * format a byte array as a hexadecimal string.
     * @param pBytes the byte array
     * @return the string
     */
    public static String bytesToHexString(final byte[] pBytes) {
        /* Allocate the string builder */
        final StringBuilder myValue = new StringBuilder(2 * pBytes.length);

        /* For each byte in the value */
        for (final byte b : pBytes) {
            /* Access the byte as an unsigned integer */
            int myInt = b;
            if (myInt < 0) {
                myInt += BYTE_MASK + 1;
            }

            /* Access the high nybble */
            int myDigit = myInt >>> NYBBLE_SHIFT;
            char myChar = Character.forDigit(myDigit, HEX_RADIX);

            /* Add it to the value string */
            myValue.append(myChar);

            /* Access the low digit */
            myDigit = myInt
                      & NYBBLE_MASK;
            myChar = Character.forDigit(myDigit, HEX_RADIX);

            /* Add it to the value string */
            myValue.append(myChar);
        }

        /* Return the string */
        return myValue.toString();
    }

    /**
     * format a long as a hexadecimal string.
     * @param pValue the long value
     * @return the string
     */
    public static String longToHexString(final long pValue) {
        /* Access the long value */
        long myLong = pValue;

        /* Allocate the string builder */
        final StringBuilder myValue = new StringBuilder();

        /* handle negative values */
        final boolean isNegative = myLong < 0;
        if (isNegative) {
            myLong = -myLong;
        }

        /* Special case for zero */
        if (myLong == 0) {
            myValue.append("00");

            /* else need to loop through the digits */
        } else {
            /* While we have digits to format */
            while (myLong > 0) {
                /* Access the digit and move to next one */
                final int myDigit = (int) (myLong & NYBBLE_MASK);
                final char myChar = Character.forDigit(myDigit, HEX_RADIX);
                myValue.insert(0, myChar);
                myLong >>>= NYBBLE_SHIFT;
            }

            /* If we are odd length prefix a zero */
            if ((myValue.length() & 1) != 0) {
                myValue.insert(0, '0');
            }

            /* Reinstate negative sign */
            if (isNegative) {
                myValue.insert(0, '-');
            }
        }

        /* Return the string */
        return myValue.toString();
    }

    /**
     * parse a byte array from a hexadecimal string.
     * @param pHexString the hex string
     * @return the bytes
     * @throws OceanusException on error
     */
    public static byte[] hexStringToBytes(final String pHexString) throws OceanusException {
        /* Access the length of the hex string */
        final int myLen = pHexString.length();

        /* Check that it has an even length */
        if (myLen % 2 != 0) {
            throw new OceanusDataException(ERROR_HEXLEN
                                          + pHexString);
        }

        /* Allocate the new bytes array */
        final byte[] myByteValue = new byte[myLen / 2];

        /* Loop through the string */
        for (int i = 0; i < myLen; i += 2) {
            /* Access the top level byte */
            char myChar = pHexString.charAt(i);
            int myDigit = Character.digit(myChar, HEX_RADIX);

            /* Check that the char is a valid hex digit */
            if (myDigit < 0) {
                throw new OceanusDataException(ERROR_HEXDIGIT
                                              + pHexString);
            }

            /* Initialise result */
            int myInt = myDigit << NYBBLE_SHIFT;

            /* Access the second byte */
            myChar = pHexString.charAt(i + 1);
            myDigit = Character.digit(myChar, HEX_RADIX);

            /* Check that the char is a valid hex digit */
            if (myDigit < 0) {
                throw new OceanusDataException(ERROR_HEXDIGIT
                                              + pHexString);
            }

            /* Add into result */
            myInt += myDigit;

            /* Convert to byte and store */
            if (myInt > Byte.MAX_VALUE) {
                myInt -= BYTE_MASK + 1;
            }
            myByteValue[i / 2] = (byte) myInt;
        }

        /* Return the bytes */
        return myByteValue;
    }

    /**
     * parse a long from a hexadecimal string.
     * @param pHexString the hex string
     * @return the bytes
     * @throws OceanusException on error
     */
    public static long hexStringToLong(final String pHexString) throws OceanusException {
        /* Access the length of the hex string */
        String myHexString = pHexString;
        int myLen = myHexString.length();

        /* handle negative values */
        final boolean isNegative = myLen > 0
                                   && myHexString.charAt(0) == '-';
        if (isNegative) {
            myHexString = myHexString.substring(1);
            myLen--;
        }

        /* Check that it has an even length */
        if (myLen % 2 != 0) {
            throw new OceanusDataException(ERROR_HEXLEN
                                          + pHexString);
        }

        /* Loop through the string */
        long myValue = 0;
        for (int i = 0; i < myLen; i++) {
            /* Access the next character */
            final char myChar = myHexString.charAt(i);
            final int myDigit = Character.digit(myChar, HEX_RADIX);

            /* Check that the char is a valid hex digit */
            if (myDigit < 0) {
                throw new OceanusDataException(ERROR_HEXDIGIT
                                              + pHexString);
            }

            /* Add into the value */
            myValue <<= NYBBLE_SHIFT;
            myValue += myDigit;
        }

        /* Reinstate negative values */
        if (isNegative) {
            myValue = -myValue;
        }

        /* Return the value */
        return myValue;
    }

    /**
     * Convert character array to byte array.
     * @param pChars the character array
     * @return the byte array
     * @throws OceanusException on error
     */
    public static byte[] charsToByteArray(final char[] pChars) throws OceanusException {
        /* protect against exceptions */
        try {
            /* Transform the character array to a byte array */
            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
            final OutputStreamWriter out = new OutputStreamWriter(baos, StandardCharsets.UTF_8);
            out.write(pChars, 0, pChars.length);
            out.flush();
            return baos.toByteArray();
        } catch (IOException e) {
            throw new OceanusDataException(e.getMessage(), e);
        }
    }

    /**
     * Convert byte array to character array.
     * @param pBytes the byte array
     * @return the character array
     * @throws OceanusException on error
     */
    public static char[] bytesToCharArray(final byte[] pBytes) throws OceanusException {
        /* protect against exceptions */
        try {
            /* Allocate the character array allowing for one character per byte */
            char[] myArray = new char[pBytes.length];

            /* Transform the byte array to a character array */
            final ByteArrayInputStream bais = new ByteArrayInputStream(pBytes);
            final InputStreamReader in = new InputStreamReader(bais, StandardCharsets.UTF_8);
            final int myLen = in.read(myArray);

            /* Cut down the array to the actual length */
            myArray = Arrays.copyOf(myArray, myLen);

            /* Return the array */
            return myArray;
        } catch (IOException e) {
            throw new OceanusDataException(e.getMessage(), e);
        }
    }

    /**
     * parse a long from a byte array.
     * @param pBytes the eight byte array holding the long
     * @return the long value
     */
    public static long byteArrayToLong(final byte[] pBytes) {
        /* Loop through the bytes */
        long myValue = 0;
        for (int i = 0; i < Long.BYTES; i++) {
            /* Access the next byte as an unsigned integer */
            int myByte = pBytes[i];
            myByte &= BYTE_MASK;

            /* Add in to value */
            myValue <<= BYTE_SHIFT;
            myValue += myByte;
        }

        /* Return the value */
        return myValue;
    }

    /**
     * build a byte array from a long.
     * @param pValue the long value to convert
     * @return the byte array
     */
    public static byte[] longToByteArray(final long pValue) {
        /* Loop through the bytes */
        long myValue = pValue;
        final byte[] myBytes = new byte[Long.BYTES];
        for (int i = Long.BYTES; i > 0; i--) {
            /* Store the next byte */
            final byte myByte = (byte) (myValue & BYTE_MASK);
            myBytes[i - 1] = myByte;

            /* Adjust value */
            myValue >>= BYTE_SHIFT;
        }

        /* Return the value */
        return myBytes;
    }

    /**
     * parse an integer from a byte array.
     * @param pBytes the four byte array holding the integer
     * @return the integer value
     */
    public static int byteArrayToInteger(final byte[] pBytes) {
        /* Loop through the bytes */
        int myValue = 0;
        for (int i = 0; i < Integer.BYTES; i++) {
            /* Access the next byte as an unsigned integer */
            int myByte = pBytes[i];
            myByte &= BYTE_MASK;

            /* Add in to value */
            myValue <<= BYTE_SHIFT;
            myValue += myByte;
        }

        /* Return the value */
        return myValue;
    }

    /**
     * build a byte array from an integer.
     * @param pValue the integer value to convert
     * @return the byte array
     */
    public static byte[] integerToByteArray(final int pValue) {
        /* Loop through the bytes */
        final byte[] myBytes = new byte[Integer.BYTES];
        int myValue = pValue;
        for (int i = Integer.BYTES; i > 0; i--) {
            /* Store the next byte */
            final byte myByte = (byte) (myValue & BYTE_MASK);
            myBytes[i - 1] = myByte;

            /* Adjust value */
            myValue >>= BYTE_SHIFT;
        }

        /* Return the value */
        return myBytes;
    }

    /**
     * parse a short from a byte array.
     * @param pBytes the four byte array holding the integer
     * @return the short value
     */
    public static short byteArrayToShort(final byte[] pBytes) {
        /* Loop through the bytes */
        short myValue = 0;
        for (int i = 0; i < Short.BYTES; i++) {
            /* Access the next byte as an unsigned integer */
            short myByte = pBytes[i];
            myByte &= BYTE_MASK;

            /* Add in to value */
            myValue <<= BYTE_SHIFT;
            myValue += myByte;
        }

        /* Return the value */
        return myValue;
    }

    /**
     * build a byte array from a short.
     * @param pValue the short value to convert
     * @return the byte array
     */
    public static byte[] shortToByteArray(final short pValue) {
        /* Loop through the bytes */
        final byte[] myBytes = new byte[Short.BYTES];
        int myValue = pValue;
        for (int i = Short.BYTES; i > 0; i--) {
            /* Store the next byte */
            final byte myByte = (byte) (myValue & BYTE_MASK);
            myBytes[i - 1] = myByte;

            /* Adjust value */
            myValue >>= BYTE_SHIFT;
        }

        /* Return the value */
        return myBytes;
    }

    /**
     * get Bytes from String.
     * @param pInput the bytes to obtain the string from
     * @return the bytes representing the bytes
     */
    public static String byteArrayToString(final byte[] pInput) {
        return new String(pInput, StandardCharsets.UTF_8);
    }

    /**
     * get Bytes from String.
     * @param pInput the string to obtain the bytes from
     * @return the bytes representing the string
     */
    public static byte[] stringToByteArray(final String pInput) {
        return pInput.getBytes(StandardCharsets.UTF_8);
    }

    /**
     * Convert a byte array to a Base64 string.
     * @param pBytes the byte array (not null)
     * @return the translated Base64 string (not null)
     */
    public static String byteArrayToBase64(final byte[] pBytes) {
        /* Determine input length and allocate output buffer */
        final int myLen = pBytes.length;
        final StringBuilder myBuilder = new StringBuilder(myLen << 1);
        final byte[] myTriplet = new byte[BASE64_TRIPLE];

        /* Loop through the input bytes */
        int myIn = 0;
        while (myIn < myLen) {
            /* Access input triplet */
            myTriplet[0] = pBytes[myIn++];
            myTriplet[1] = myIn < myLen
                                        ? pBytes[myIn++]
                                        : 0;
            myTriplet[2] = myIn < myLen
                                        ? pBytes[myIn++]
                                        : 0;

            /* Convert to base64 */
            myBuilder.append(BASE64_ENCODE[(myTriplet[0] >> BASE64_SHIFT1)
                                           & BASE64_MASK]);
            myBuilder.append(BASE64_ENCODE[((myTriplet[0] << BASE64_SHIFT2) | ((myTriplet[1] & BYTE_MASK) >> BASE64_SHIFT2))
                                           & BASE64_MASK]);
            myBuilder.append(BASE64_ENCODE[((myTriplet[1] << BASE64_SHIFT1) | ((myTriplet[2] & BYTE_MASK) >> BASE64_SHIFT3))
                                           & BASE64_MASK]);
            myBuilder.append(BASE64_ENCODE[myTriplet[2]
                                           & BASE64_MASK]);
        }

        /* Handle short input */
        int myXtra = myLen
                     % myTriplet.length;
        if (myXtra > 0) {
            /* Determine padding length */
            myXtra = myTriplet.length
                     - myXtra;

            /* Remove redundant characters */
            myBuilder.setLength(myBuilder.length()
                                - myXtra);

            /* Replace with padding character */
            while (myXtra-- > 0) {
                myBuilder.append(BASE64_PAD);
            }
        }

        /* Convert chars to string */
        return myBuilder.toString();
    }

    /**
     * Convert a Base64 string into a byte array.
     * @param pBase64 the Base64 string (not null)
     * @return the byte array (not null)
     */
    public static byte[] base64ToByteArray(final String pBase64) {
        /* Access input as chars */
        final char[] myBase64 = pBase64.toCharArray();
        final int myLen = myBase64.length;

        /* Determine number of padding bytes */
        int myNumPadding = 0;
        if (myBase64[myLen - 1] == BASE64_PAD) {
            myNumPadding++;
            if (myBase64[myLen - 2] == BASE64_PAD) {
                myNumPadding++;
            }
        }

        /* Allocate the output buffer and index */
        final int myOutLen = ((myLen * BASE64_TRIPLE) >> 2)
                             - myNumPadding;
        final byte[] myOutput = new byte[myOutLen];

        /* Loop through the base64 input */
        int myIn = 0;
        int myOut = 0;
        while (myOut < myOutLen) {
            /* Build first byte */
            final int c0 = BASE64_DECODE[myBase64[myIn++]];
            final int c1 = BASE64_DECODE[myBase64[myIn++]];
            myOutput[myOut++] = (byte) (((c0 << BASE64_SHIFT1) | (c1 >> BASE64_SHIFT2)) & BYTE_MASK);

            /* Build second byte */
            if (myOut < myOutLen) {
                final int c2 = BASE64_DECODE[myBase64[myIn++]];
                myOutput[myOut++] = (byte) (((c1 << BASE64_SHIFT2) | (c2 >> BASE64_SHIFT1)) & BYTE_MASK);

                /* Build third byte */
                if (myOut < myOutLen) {
                    final int c3 = BASE64_DECODE[myBase64[myIn++]];
                    myOutput[myOut++] = (byte) (((c2 << BASE64_SHIFT3) | c3) & BYTE_MASK);
                }
            }
        }
        return myOutput;
    }
}