Java tutorial
/* * 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; } }