FileDocCategorySizeDatePackage
Utils.javaAPI DocJaudiotagger 2.0.419304Wed Mar 30 16:11:52 BST 2011org.jaudiotagger.audio.asf.util

Utils.java

/*
 * Entagged Audio Tag library
 * Copyright (c) 2004-2005 Christian Laireiter <liree@web.de>
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *  
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package org.jaudiotagger.audio.asf.util;

import org.jaudiotagger.audio.asf.data.AsfHeader;
import org.jaudiotagger.audio.asf.data.GUID;
import org.jaudiotagger.logging.ErrorMessage;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

/**
 * Some static Methods which are used in several Classes. <br>
 * 
 * @author Christian Laireiter
 */
public class Utils {

    public static final long DIFF_BETWEEN_ASF_DATE_AND_JAVA_DATE = 11644470000000l;
    /**
     * Stores the default line separator of the current underlying system.
     */
    public final static String LINE_SEPARATOR = System
            .getProperty("line.separator"); //$NON-NLS-1$
    /**
     * 
     */
    private static final int MAXIMUM_STRING_LENGTH_ALLOWED = 32766;

    /**
     * This method checks given string will not exceed limit in bytes[] when
     * converted UTF-16LE encoding (2 bytes per character) and checks whether
     * the length doesn't exceed 65535 bytes. <br>
     * 
     * @param value
     *            The string to check.
     * @throws IllegalArgumentException
     *             If byte representation takes more than 65535 bytes.
     */
    public static void checkStringLengthNullSafe(String value)
            throws IllegalArgumentException {
        if (value != null) {
            if (value.length() > MAXIMUM_STRING_LENGTH_ALLOWED) {
                throw new IllegalArgumentException(
                        ErrorMessage.WMA_LENGTH_OF_STRING_IS_TOO_LARGE
                                .getMsg((value.length() * 2)));
            }
        }
    }

    /**
     * 
     * @param value String to check for null
     * @return true unless string is too long
     */
    public static boolean isStringLengthValidNullSafe(String value) {
        if (value != null) {
            if (value.length() > MAXIMUM_STRING_LENGTH_ALLOWED) {
                return false;
            }
        }
        return true;
    }

    /**
     * effectively copies a specified amount of bytes from one stream to
     * another.
     * 
     * @param source
     *            stream to read from
     * @param dest
     *            stream to write to
     * @param amount
     *            amount of bytes to copy
     * @throws IOException
     *             on I/O errors, and if the source stream depletes before all
     *             bytes have been copied.
     */
    public static void copy(InputStream source, OutputStream dest, long amount)
            throws IOException {
        byte[] buf = new byte[8192];
        long copied = 0;
        while (copied < amount) {
            int toRead = 8192;
            if ((amount - copied) < 8192) {
                toRead = (int) (amount - copied);
            }
            int read = source.read(buf, 0, toRead);
            if (read == -1) {
                throw new IOException(
                        "Inputstream has to continue for another "
                                + (amount - copied) + " bytes.");
            }
            dest.write(buf, 0, read);
            copied += read;
        }
    }

    /**
     * Copies all of the source to the destination.<br>
     * 
     * @param source
     *            source to read from
     * @param dest
     *            stream to write to
     * @throws IOException
     *             on I/O errors.
     */
    public static void flush(final InputStream source, final OutputStream dest)
            throws IOException {
        final byte[] buf = new byte[8192];
        int read;
        while ((read = source.read(buf)) != -1) {
            dest.write(buf, 0, read);
        }
    }

    /**
     * This method will create a byte[] at the size of <code>byteCount</code>
     * and insert the bytes of <code>value</code> (starting from lowset byte)
     * into it. <br>
     * You can easily create a Word (16-bit), DWORD (32-bit), QWORD (64 bit) out
     * of the value, ignoring the original type of value, since java
     * automatically performs transformations. <br>
     * <b>Warning: </b> This method works with unsigned numbers only.
     * 
     * @param value
     *            The value to be written into the result.
     * @param byteCount
     *            The number of bytes the array has got.
     * @return A byte[] with the size of <code>byteCount</code> containing the
     *         lower byte values of <code>value</code>.
     */
    public static byte[] getBytes(final long value, final int byteCount) {
        byte[] result = new byte[byteCount];
        for (int i = 0; i < result.length; i ++) {
            result[i] = (byte) ((value >>> (i*8)) & 0xFF);
        }
        return result;
    }

    /**
     * Convenience method to convert the given string into a byte sequence which
     * has the format of the charset given.
     * 
     * @param source
     *            string to convert.
     * @param charset
     *            charset to apply
     * @return the source's binary representation according to the charset.
     */
    public static byte[] getBytes(final String source, final Charset charset) {
        assert charset != null;
        assert source != null;
        final ByteBuffer encoded = charset.encode(source);
        final byte[] result = new byte[encoded.limit()];
        encoded.rewind();
        encoded.get(result);
        return result;
    }

    /**
     * Since date values in ASF files are given in 100 ns steps since first
     * january of 1601 a little conversion must be done. <br>
     * This method converts a date given in described manner to a calendar.
     * 
     * @param fileTime
     *            Time in 100ns since 1 jan 1601
     * @return Calendar holding the date representation.
     */
    /* Old method that ran very slowely and doesnt logical correct, how does dividing something
      at 10-4 by 10,000 convert it to 10 -3
    public static GregorianCalendar getDateOf(final BigInteger fileTime) {
        final GregorianCalendar result = new GregorianCalendar(1601, 0, 1);
        // lose anything beyond milliseconds, because calendar can't handle
        // less value
        BigInteger time = fileTime.divide(new BigInteger("10000")); //$NON-NLS-1$
        final BigInteger maxInt = new BigInteger(String
                .valueOf(Integer.MAX_VALUE));
        while (time.compareTo(maxInt) > 0) {
            result.add(Calendar.MILLISECOND, Integer.MAX_VALUE);
            time = time.subtract(maxInt);
        }
        result.add(Calendar.MILLISECOND, time.intValue());
        return result;
    }
    */

    /**
     * Date values in ASF files are given in 100 ns (10 exp -4) steps since first
     *
     * @param fileTime
     *            Time in 100ns since 1 jan 1601
     * @return Calendar holding the date representation.
     */
    public static GregorianCalendar getDateOf(final BigInteger fileTime) {
        final GregorianCalendar result = new GregorianCalendar();

        // Divide by 10 to convert from -4 to -3 (millisecs)
        BigInteger time = fileTime.divide(new BigInteger("10"));
        // Construct Date taking into the diff between 1601 and 1970
        Date date = new Date(time.longValue() - DIFF_BETWEEN_ASF_DATE_AND_JAVA_DATE);
        result.setTime(date);
        return result;
    }

    /**
     * Tests if the given string is <code>null</code> or just contains
     * whitespace characters.
     * 
     * @param toTest
     *            String to test.
     * @return see description.
     */
    public static boolean isBlank(String toTest) {
        if (toTest == null)
        {
            return true;
        }

        for (int i = 0; i < toTest.length(); i++)
        {
            if(!Character.isWhitespace(toTest.charAt(i)))
            {
                return false;
            }
        }
        return true;
    }

    /**
     * Reads 8 bytes from stream and interprets them as a UINT64 which is
     * returned as {@link BigInteger}.<br>
     * 
     * @param stream
     *            stream to readm from.
     * @return a BigInteger which represents the read 8 bytes value.
     * @throws IOException if problem reading bytes
     */
    public static BigInteger readBig64(InputStream stream) throws IOException {
        byte[] bytes = new byte[8];
        byte[] oa = new byte[8];
        int read = stream.read(bytes);
        if (read != 8) {
            // 8 bytes mandatory.
            throw new EOFException();
        }
        for (int i = 0; i < bytes.length; i++) {
            oa[7 - i] = bytes[i];
        }
        return new BigInteger(oa);
    }

    /**
     * Reads <code>size</code> bytes from the stream.<br>
     * 
     * @param stream
     *            stream to read from.
     * @param size
     *            amount of bytes to read.
     * @return the read bytes.
     * @throws IOException
     *             on I/O errors.
     */
    public static byte[] readBinary(InputStream stream, long size)
            throws IOException {
        byte[] result = new byte[(int) size];
        stream.read(result);
        return result;
    }

    /**
     * This method reads a UTF-16 String, which length is given on the number of
     * characters it consists of. <br>
     * The stream must be at the number of characters. This number contains the
     * terminating zero character (UINT16).
     * 
     * @param stream
     *            Input source
     * @return String
     * @throws IOException
     *             read errors
     */
    public static String readCharacterSizedString(InputStream stream)
            throws IOException {
        StringBuilder result = new StringBuilder();
        int strLen = readUINT16(stream);
        int character = stream.read();
        character |= stream.read() << 8;
        do {
            if (character != 0) {
                result.append((char) character);
                character = stream.read();
                character |= stream.read() << 8;
            }
        } while (character != 0 || (result.length() + 1) > strLen);
        if (strLen != (result.length() + 1)) {
            throw new IllegalStateException(
                    "Invalid Data for current interpretation"); //$NON-NLS-1$
        }
        return result.toString();
    }

    /**
     * This method reads a UTF-16 encoded String. <br>
     * For the use this method the number of bytes used by current string must
     * be known. <br>
     * The ASF specification recommends that those strings end with a
     * terminating zero. However it also says that it is not always the case.
     * 
     * @param stream
     *            Input source
     * @param strLen
     *            Number of bytes the String may take.
     * @return read String.
     * @throws IOException
     *             read errors.
     */
    public static String readFixedSizeUTF16Str(InputStream stream, int strLen)
            throws IOException {
        byte[] strBytes = new byte[strLen];
        int read = stream.read(strBytes);
        if (read == strBytes.length) {
            if (strBytes.length >= 2) {
                /*
                 * Zero termination is recommended but optional. So check and
                 * if, remove.
                 */
                if (strBytes[strBytes.length - 1] == 0
                        && strBytes[strBytes.length - 2] == 0) {
                    byte[] copy = new byte[strBytes.length - 2];
                    System.arraycopy(strBytes, 0, copy, 0, strBytes.length - 2);
                    strBytes = copy;
                }
            }
            return new String(strBytes, "UTF-16LE");
        }
        throw new IllegalStateException(
                "Couldn't read the necessary amount of bytes.");
    }

    /**
     * This Method reads a GUID (which is a 16 byte long sequence) from the
     * given <code>raf</code> and creates a wrapper. <br>
     * <b>Warning </b>: <br>
     * There is no way of telling if a byte sequence is a guid or not. The next
     * 16 bytes will be interpreted as a guid, whether it is or not.
     * 
     * @param stream
     *            Input source.
     * @return A class wrapping the guid.
     * @throws IOException
     *             happens when the file ends before guid could be extracted.
     */
    public static GUID readGUID(InputStream stream) throws IOException {
        if (stream == null) {
            throw new IllegalArgumentException("Argument must not be null"); //$NON-NLS-1$
        }
        int[] binaryGuid = new int[GUID.GUID_LENGTH];
        for (int i = 0; i < binaryGuid.length; i++) {
            binaryGuid[i] = stream.read();
        }
        return new GUID(binaryGuid);
    }

    /**
     * Reads 2 bytes from stream and interprets them as UINT16.<br>
     * 
     * @param stream
     *            stream to read from.
     * @return UINT16 value
     * @throws IOException
     *             on I/O Errors.
     */
    public static int readUINT16(InputStream stream) throws IOException {
        int result = stream.read();
        result |= stream.read() << 8;
        return result;
    }

    /**
     * Reads 4 bytes from stream and interprets them as UINT32.<br>
     * 
     * @param stream
     *            stream to read from.
     * @return UINT32 value
     * @throws IOException
     *             on I/O Errors.
     */
    public static long readUINT32(InputStream stream) throws IOException {
        long result = 0;
        for (int i = 0; i <= 24; i += 8) {
            // Warning, always cast to long here. Otherwise it will be
            // shifted as int, which may produce a negative value, which will
            // then be extended to long and assign the long variable a negative
            // value.
            result |= (long) stream.read() << i;
        }
        return result;
    }

    /**
     * Reads long as little endian.
     * 
     * @param stream
     *            Data source
     * @return long value
     * @throws IOException
     *             read error, or eof is reached before long is completed
     */
    public static long readUINT64(InputStream stream) throws IOException {
        long result = 0;
        for (int i = 0; i <= 56; i += 8) {
            // Warning, always cast to long here. Otherwise it will be
            // shifted as int, which may produce a negative value, which will
            // then be extended to long and assign the long variable a negative
            // value.
            result |= (long) stream.read() << i;
        }
        return result;
    }

    /**
     * This method reads a UTF-16 encoded String, beginning with a 16-bit value
     * representing the number of bytes needed. The String is terminated with as
     * 16-bit ZERO. <br>
     * 
     * @param stream
     *            Input source
     * @return read String.
     * @throws IOException
     *             read errors.
     */
    public static String readUTF16LEStr(InputStream stream) throws IOException {
        int strLen = readUINT16(stream);
        byte[] buf = new byte[strLen];
        int read = stream.read(buf);
        if (read == strLen || (strLen == 0 && read == -1)) {
            /*
             * Check on zero termination
             */
            if (buf.length >= 2) {
                if (buf[buf.length - 1] == 0 && buf[buf.length - 2] == 0) {
                    byte[] copy = new byte[buf.length - 2];
                    System.arraycopy(buf, 0, copy, 0, buf.length - 2);
                    buf = copy;
                }
            }
            return new String(buf, AsfHeader.ASF_CHARSET.name());
        }
        throw new IllegalStateException(
                "Invalid Data for current interpretation"); //$NON-NLS-1$
    }

    /**
     * Writes the given value as UINT16 into the stream.
     * 
     * @param number
     *            value to write.
     * @param out
     *            stream to write into.
     * @throws IOException
     *             On I/O errors
     */
    public static void writeUINT16(int number, OutputStream out)
            throws IOException {
        if (number < 0) {
            throw new IllegalArgumentException("positive value expected."); //$NON-NLS-1$
        }
        byte[] toWrite = new byte[2];
        for (int i = 0; i <= 8; i += 8) {
            toWrite[i / 8] = (byte) ((number >> i) & 0xFF);
        }
        out.write(toWrite);
    }

    /**
     * Writes the given value as UINT32 into the stream.
     * 
     * @param number
     *            value to write.
     * @param out
     *            stream to write into.
     * @throws IOException
     *             On I/O errors
     */
    public static void writeUINT32(long number, OutputStream out)
            throws IOException {
        if (number < 0) {
            throw new IllegalArgumentException("positive value expected."); //$NON-NLS-1$
        }
        byte[] toWrite = new byte[4];
        for (int i = 0; i <= 24; i += 8) {
            toWrite[i / 8] = (byte) ((number >> i) & 0xFF);
        }
        out.write(toWrite);
    }

    /**
     * Writes the given value as UINT64 into the stream.
     * 
     * @param number
     *            value to write.
     * @param out
     *            stream to write into.
     * @throws IOException
     *             On I/O errors
     */
    public static void writeUINT64(long number, OutputStream out)
            throws IOException {
        if (number < 0) {
            throw new IllegalArgumentException("positive value expected."); //$NON-NLS-1$
        }
        byte[] toWrite = new byte[8];
        for (int i = 0; i <= 56; i += 8) {
            toWrite[i / 8] = (byte) ((number >> i) & 0xFF);
        }
        out.write(toWrite);
    }

}