org.ardverk.daap.DaapUtil.java Source code

Java tutorial

Introduction

Here is the source code for org.ardverk.daap.DaapUtil.java

Source

/*
 * Digital Audio Access Protocol (DAAP) Library
 * Copyright (C) 2004-2010 Roger Kapsi
 *
 * 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 org.ardverk.daap;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.StringTokenizer;
import java.util.zip.GZIPOutputStream;

import org.apache.http.Header;
import org.apache.http.impl.auth.DigestScheme;
import org.ardverk.daap.chunks.Chunk;
import org.ardverk.daap.chunks.UIntChunk;

/**
 * Misc methods and constants
 * 
 * @author Roger Kapsi
 */
public final class DaapUtil {

    /** */
    private static final Random generator = new Random();

    /**
     * NULL value (Zero) is a forbidden value (in some cases) in DAAP and means
     * that a value is not initialized (basically <code>null</code> for
     * primitive types).
     */
    public static final int NULL = 0;

    /**
     * Global flag to turn gzip compression on and off
     */
    public static final boolean COMPRESS = true;

    /** ISO Latin 1 encoding */
    public static final String ISO_8859_1 = "ISO-8859-1";

    /** UTF-8 encoding */
    public static final String UTF_8 = "UTF-8";

    /** "\r\n" <b>DON'T TOUCH!</b> */
    static final byte[] CRLF = { (byte) '\r', (byte) '\n' };

    private final static SimpleDateFormat formatter = new SimpleDateFormat("EEE, d MMM yyyy hh:mm:ss z", Locale.US);

    /** DAAP 1.0.0 (iTunes 4.0) */
    public static final int DAAP_VERSION_1 = 0x00010000; // 1.0.0

    /** DAAP 2.0.0 (iTunes 4.1, 4.2) */
    public static final int DAAP_VERSION_2 = 0x00020000; // 2.0.0

    /** DAAP Version 3.0.0 (iTunes 4.5, 4.6) */
    public static final int DAAP_VERSION_3 = 0x00030000; // 3.0.0

    /** DAAP Version 3.0.2 (iTunes 5.0) */
    public static final int DAAP_VERSION_302 = 0x003002; // 3.0.2

    /** DMAP Version 2.0.1 */
    public static final int DMAP_VERSION_201 = 0x00020001; // 2.0.1

    /** DMAP Version 2.0.1 (iTunes 5.0) */
    public static final int DMAP_VERSION_202 = 0x00020002; // 2.0.2

    /** Music Sharing Version 2.0.1 */
    public static final int MUSIC_SHARING_VERSION_201 = 0x00020001; // 2.0.1

    /** 0, 1, ... F */
    private static final char[] HEX = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E',
            'F' };

    /** Default DAAP realm */
    static final String DAAP_REALM = "daap";

    /**
     * List of sharable formats/extensions. The list is likely not complete!
     * 
     * TODO: complete list
     */
    private static final String[] SUPPORTED_FORMATS = { ".mp3", ".m4a", ".m4p", ".wav", ".aif", ".aiff", ".m1a" };

    private DaapUtil() {
    }

    /**
     * Returns <code>true</code> if version is a supported protocol version. At
     * the moment only {@see #VERSION_3} and later are supported.
     * 
     * @param version
     *            a protocol version
     * @return <code>true</code> if version is a supported
     */
    public static boolean isSupportedProtocolVersion(int version) {
        if (version >= DAAP_VERSION_3) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Converts a four character content code to an int and returns it.
     * 
     * @param contentCode
     *            a four character content code
     * @return content code
     */
    public static int toContentCodeNumber(String contentCode) {
        if (contentCode.length() != 4) {
            throw new IllegalArgumentException("content code must have 4 characters!");
        }

        return ((contentCode.charAt(0) & 0xFF) << 24) | ((contentCode.charAt(1) & 0xFF) << 16)
                | ((contentCode.charAt(2) & 0xFF) << 8) | ((contentCode.charAt(3) & 0xFF));
    }

    /**
     * Converts an four byte int to a string
     */
    public static String toContentCodeString(int contentCode) {
        char[] code = new char[4];
        code[0] = (char) ((contentCode >> 24) & 0xFF);
        code[1] = (char) ((contentCode >> 16) & 0xFF);
        code[2] = (char) ((contentCode >> 8) & 0xFF);
        code[3] = (char) ((contentCode) & 0xFF);
        return new String(code);
    }

    /**
     * Returns the current Date/Time in "iTunes time format"
     */
    public static final String now() {
        return formatter.format(new Date());
    }

    /**
     * Serializes the <code>chunk</code> and compresses it optionally. The
     * serialized data is returned as a byte-Array.
     */
    public static final byte[] serialize(Chunk chunk, boolean compress) throws IOException {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream(255);
        DaapOutputStream out = null;

        if (DaapUtil.COMPRESS && compress) {
            GZIPOutputStream gzip = new GZIPOutputStream(buffer);
            out = new DaapOutputStream(gzip);
        } else {
            out = new DaapOutputStream(buffer);
        }

        out.writeChunk(chunk);
        out.close();

        return buffer.toByteArray();
    }

    /**
     * Splits a query String ("key1=value1&key2=value2...") and stores the data
     * in a Map
     * 
     * @param queryString
     *            a query String
     * @return the splitten query String as Map
     */
    public static final Map<String, String> parseQuery(String queryString) {

        Map<String, String> map = new HashMap<String, String>();

        if (queryString != null && queryString.length() != 0) {
            StringTokenizer tok = new StringTokenizer(queryString, "&");
            while (tok.hasMoreTokens()) {
                String token = tok.nextToken();

                int q = token.indexOf('=');
                if (q != -1 && q != token.length()) {
                    String key = token.substring(0, q);
                    String value = token.substring(++q);
                    map.put(key, value);
                }
            }
        }

        return map;
    }

    /**
     * Splits a meta String ("foo,bar,alice,bob") and stores the data in an
     * ArrayList
     * 
     * @param meta
     *            a meta String
     * @return the splitten meta String as ArrayList
     */
    public static final List<String> parseMeta(String meta) {
        StringTokenizer tok = new StringTokenizer(meta, ",");
        List<String> list = new ArrayList<String>(tok.countTokens());
        boolean flag = false;

        while (tok.hasMoreTokens()) {
            String token = tok.nextToken();

            // Must be te fist! See DAAP documentation
            // for more info!
            if (!flag && token.equals("dmap.itemkind")) {
                list.add(0, token);
                flag = true;
            } else {
                list.add(token);
            }
        }
        return list;
    }

    /**
     * Converts major, minor to a DAAP version. Version 2 is for example
     * 0x00020000
     * 
     * @param major
     *            the major version (x)
     * @return x.0.0
     */
    public static int toVersion(int major) {
        return toVersion(major, 0, 0);
    }

    /**
     * Converts major, minor to a DAAP version. Version 2.1 is for example
     * 0x00020100
     * 
     * @param major
     *            the major version (x)
     * @param minor
     *            the minor version (y)
     * @return x.y.0
     */
    public static int toVersion(int major, int minor) {
        return toVersion(major, minor, 0);
    }

    /**
     * Converts major, minor and patch to a DAAP version. Version 2.1.3 is for
     * example 0x00020103
     * 
     * @param major
     *            the major version (x)
     * @param minor
     *            the minor version (y)
     * @param micro
     *            the patch version (z)
     * @return x.y.z
     */
    public static int toVersion(int major, int minor, int micro) {
        return (major & 0xFFFF) << 16 | (minor & 0xFF) << 8 | (micro & 0xFF);
    }

    /**
     * This method tries the determinate the protocol version and returns it or
     * {@see #NULL} if version could not be estimated...
     */
    public static int getProtocolVersion(DaapRequest request) {

        if (request.isUnknownRequest())
            return DaapUtil.NULL;

        Header header = request.getHeader(DaapRequest.CLIENT_DAAP_VERSION);

        if (header == null && request.isSongRequest()) {
            header = request.getHeader(DaapRequest.USER_AGENT);
        }

        if (header == null)
            return DaapUtil.NULL;

        String name = header.getName();
        String value = header.getValue();

        // Unfortunately song requests do not have a Client-DAAP-Version
        // header. As a workaround we can estimate the protocol version
        // by User-Agent but that is weak an may break with non iTunes
        // hosts...
        if (request.isSongRequest() && name.equals(DaapRequest.USER_AGENT)) {

            // Note: the protocol version of a Song request is estimated
            // by the server with the aid of the sessionId, i.e. this block
            // is actually never touched...
            if (value.startsWith("iTunes/5.0")) {
                return DaapUtil.DAAP_VERSION_302;
            } else if (value.startsWith("iTunes/4.9") || value.startsWith("iTunes/4.8")
                    || value.startsWith("iTunes/4.7") || value.startsWith("iTunes/4.6")
                    || value.startsWith("iTunes/4.5")) {
                return DaapUtil.DAAP_VERSION_3;
            } else if (value.startsWith("iTunes/4.2") || value.startsWith("iTunes/4.1")) {
                return DaapUtil.DAAP_VERSION_2;
            } else if (value.startsWith("iTunes/4.0")) {
                return DaapUtil.DAAP_VERSION_1;
            } else {
                return DaapUtil.NULL;
            }
        } else {

            StringTokenizer tokenizer = new StringTokenizer(value, ".");
            int count = tokenizer.countTokens();

            if (count >= 2 && count <= 3) {
                try {

                    int major = DaapUtil.NULL;
                    int minor = DaapUtil.NULL;
                    int patch = DaapUtil.NULL;

                    major = Integer.parseInt(tokenizer.nextToken());
                    minor = Integer.parseInt(tokenizer.nextToken());

                    if (count == 3)
                        patch = Integer.parseInt(tokenizer.nextToken());

                    return DaapUtil.toVersion(major, minor, patch);

                } catch (NumberFormatException err) {
                }
            }
        }

        return DaapUtil.NULL;
    }

    /**
     *
     */
    public static long parseUInt(String value) throws NumberFormatException {
        try {
            return UIntChunk.checkUIntRange(Long.parseLong(value));
        } catch (IllegalArgumentException err) {
            throw new NumberFormatException("For input: " + value);
        }
    }

    /**
     * Generates a random int
     */
    public static int nextInt() {
        synchronized (generator) {
            return generator.nextInt();
        }
    }

    /**
     * Generates a random int
     */
    public static int nextInt(int max) {
        synchronized (generator) {
            return generator.nextInt(max);
        }
    }

    /**
     * String to byte Array
     */
    public static byte[] getBytes(String s, String charsetName) {
        try {
            return s.getBytes(charsetName);
        } catch (UnsupportedEncodingException e) {
            // should never happen
            throw new RuntimeException(e);
        }
    }

    /**
     * Byte Array to String
     */
    public static String toString(byte[] b, String charsetName) {
        try {
            return new String(b, charsetName);
        } catch (UnsupportedEncodingException e) {
            // should never happen
            throw new RuntimeException(e);
        }
    }

    /**
     * Returns b as hex String
     */
    public static String toHexString(byte[] b) {
        if (b.length % 2 != 0) {
            throw new IllegalArgumentException("Argument's length must be power of 2");
        }

        StringBuffer buffer = new StringBuffer(b.length * 2);
        for (int i = 0; i < b.length; i++) {
            char hi = HEX[((b[i] >> 4) & 0xF)];
            char lo = HEX[b[i] & 0xF];

            buffer.append(hi).append(lo);
        }
        return buffer.toString();
    }

    public static byte[] parseHexString(String s) {
        if (s.length() % 2 != 0) {
            throw new IllegalArgumentException("Argument's length() must be power of 2");
        }

        byte[] buffer = new byte[s.length() / 2];
        for (int i = 0, j = 0; i < buffer.length; i++) {
            buffer[i] = (byte) ((parseHexToInt(s.charAt(j++) & 0xFF) << 0x4) | parseHexToInt(s.charAt(j++) & 0xFF));
        }
        return buffer;
    }

    private static int parseHexToInt(int hex) {
        switch (hex) {
        case '0':
            return 0;
        case '1':
            return 1;
        case '2':
            return 2;
        case '3':
            return 3;
        case '4':
            return 4;
        case '5':
            return 5;
        case '6':
            return 6;
        case '7':
            return 7;
        case '8':
            return 8;
        case '9':
            return 9;
        case 'A':
            return 10;
        case 'a':
            return 10;
        case 'B':
            return 11;
        case 'b':
            return 11;
        case 'C':
            return 12;
        case 'c':
            return 12;
        case 'D':
            return 13;
        case 'd':
            return 13;
        case 'E':
            return 14;
        case 'e':
            return 14;
        case 'F':
            return 15;
        case 'f':
            return 15;
        default:
            throw new NumberFormatException("'" + Character.toString((char) hex) + "'");
        }
    }

    /**
     * Creates a random nonce
     */
    public static String nonce() {
        return DigestScheme.createCnonce();
    }

    public static byte[] toMD5(String s) {
        try {
            return MessageDigest.getInstance("MD5").digest(getBytes(s, ISO_8859_1));
        } catch (NoSuchAlgorithmException err) {
            // should never happen
            throw new RuntimeException(err);
        }
    }

    public static String calculateHA1(String username, String password) {
        return calculateHA1(username, getBytes(password, ISO_8859_1));
    }

    public static String calculateHA1(String username, byte[] password) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(getBytes(username, ISO_8859_1));
            md.update((byte) ':');
            md.update(getBytes(DAAP_REALM, ISO_8859_1));
            md.update((byte) ':');
            // md.update(getBytes(password, ISO_8859_1));
            md.update(password);
            return toHexString(md.digest());
        } catch (NoSuchAlgorithmException err) {
            // should never happen
            throw new RuntimeException(err);
        }
    }

    public static String calculateHA2(String uri) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");

            md.update(getBytes("GET", ISO_8859_1));
            md.update((byte) ':');
            md.update(getBytes(uri, ISO_8859_1));
            return toHexString(md.digest());
        } catch (NoSuchAlgorithmException err) {
            // should never happen
            throw new RuntimeException(err);
        }
    }

    public static String digest(String ha1, String ha2, String nonce) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");

            md.update(getBytes(ha1, ISO_8859_1));
            md.update((byte) ':');
            md.update(getBytes(nonce, ISO_8859_1));
            md.update((byte) ':');
            md.update(getBytes(ha2, ISO_8859_1));
            return toHexString(md.digest());
        } catch (NoSuchAlgorithmException err) {
            // should never happen
            throw new RuntimeException(err);
        }
    }

    // see org.apache.commons.httpclient.auth.DigestScheme
    /*
     * public static String digest(String username, byte[] password, String
     * nonce, String uri) { try { MessageDigest md =
     * MessageDigest.getInstance("MD5");
     * 
     * md.update(getBytes(username, ISO_8859_1)); md.update((byte)':');
     * md.update(getBytes(DAAP_REALM, ISO_8859_1)); md.update((byte)':');
     * //md.update(getBytes(password, ISO_8859_1)); md.update(password); final
     * String HA1 = toHexString(md.digest()); md.reset();
     * 
     * md.update(getBytes("GET", ISO_8859_1)); md.update((byte)':');
     * md.update(getBytes(uri, ISO_8859_1)); final String HA2 =
     * toHexString(md.digest()); md.reset();
     * 
     * md.update(getBytes(HA1, ISO_8859_1)); md.update((byte)':');
     * md.update(getBytes(nonce, ISO_8859_1)); md.update((byte)':');
     * md.update(getBytes(HA2, ISO_8859_1)); return toHexString(md.digest()); }
     * catch (NoSuchAlgorithmException err) { // should never happen throw new
     * RuntimeException(err); } }
     * 
     * /** Returns the extension of file or null if file has no extension
     */
    public static String getExtension(File file) {
        return file.isFile() ? getExtension(file.getName()) : null;
    }

    /**
     * Returns the extension of fileName or null if file has no extension
     */
    public static String getExtension(String fileName) {
        int p = fileName.lastIndexOf('.');
        if (p != -1 && ++p < fileName.length()) {
            return fileName.substring(p).toLowerCase(Locale.US);
        }
        return null;
    }

    /**
     * Returns true if file is a supported format
     */
    public static boolean isSupportedFormat(File file) {
        return file.isFile() && isSupportedFormat(file.getName());
    }

    /**
     * Returns true if fileName is a supported format
     */
    public static boolean isSupportedFormat(String fileName) {
        fileName = fileName.toLowerCase(Locale.US);
        for (int i = 0; i < SUPPORTED_FORMATS.length; i++) {
            if (fileName.endsWith(SUPPORTED_FORMATS[i])) {
                return true;
            }
        }
        return false;
    }
}