FileDocCategorySizeDatePackage
BluetoothUrl.javaAPI DocphoneME MR2 API (J2ME)21453Wed May 02 18:00:30 BST 2007com.sun.midp.io

BluetoothUrl.java

/*
 *   
 *
 * Copyright  1990-2007 Sun Microsystems, Inc. All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version
 * 2 only, as published by the Free Software Foundation.
 * 
 * This program 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
 * General Public License version 2 for more details (a copy is
 * included at /legal/license.txt).
 * 
 * You should have received a copy of the GNU General Public License
 * version 2 along with this work; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA
 * 
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
 * Clara, CA 95054 or visit www.sun.com if you need additional
 * information or have any questions.
 */
package com.sun.midp.io;

import javax.bluetooth.UUID;
import javax.bluetooth.BluetoothConnectionException;
import java.util.Hashtable;
import com.sun.kvem.jsr082.bluetooth.SDP;

/**
 * Represents a bluetooth url, i.e. connection string.
 * There are two ways of usage. First one is constructing it giving
 * url string in order to parse it into a set of fields. Second one
 * is constructig it giving fields values in order to get string
 * representation. Whenever incompatible url parts are found
 * <code>IllegalArgumentException</code> is thrown.
 */
public class BluetoothUrl {
    /** Indicates if it is a sever connection string. */
    public boolean isServer = false;

    /** Keeps server address for client url, "localhost" for server. */
    public String address = null;
    /** PSM for L2CAP or channel id for RFCOMM. */
    public int port = -1;
    /** Master parameter, true by default for server. */
    public boolean master = false;
    /** Encrypt parameter. */
    public boolean encrypt = false;
    /** Authenticate parameter. */
    public boolean authenticate = false;

    /** Value to indicate L2CAP protocol. */
    public static final int L2CAP = 0;
    /** Value to indicate RFCOMM protocol. */
    public static final int RFCOMM = 1;
    /** Value to indicate OBEX protocol. */
    public static final int OBEX = 2;
    /** Value to indicate unknown protocol. */
    public static final int UNKNOWN = 3;
    /** Indicates protocol type. */
    public int protocol = UNKNOWN;

    /** Amount of protocols supported. */
    private static final int PROTOCOLS_AMOUNT = 3;
    /**
     * Keeps protocols indicating strings.
     * Usage:
     * <code>protocolName[L2CAP]</code> to get "l2cap"
     */
    private static final String[] protocolName =
        { "btl2cap://", "btspp://", "btgoep://" };

    /**
     * Keeps uuid from server connection string,
     * <code>null</code> for client's one.
     * L2CAP, RFCOMM specific.
     */
    public String uuid = null;

    /**
     * Name parameter of server url, <code>null</code> for client's one.
     * L2CAP, RFCOMM specific.
     */
    public String name = null;

    /** Url string to parse, lower case. */
    private String url;
    /**
     * Url string to parse, original case.
     * Required for correct "name" parameter parsing for it is case-sensitive.
     */
    public String caseSensitiveUrl;

    /** Authorize parameter. L2CAP specific. */
    public boolean authorize = false;
    /** RecieveMTU parameter. L2CAP specific. */
    public int receiveMTU = -1;
    /** TransmitMTU parameter. L2CAP specific. */
    public int transmitMTU = -1;

    /** UUID value to create a transport for Service Discovery Protocol. */
    public static final UUID UUID_SDP = new UUID(0x0001);

    /** Indicates if an explicit "authenticate" parameter found. */
    private boolean explicitAuthenticate = false;

    /** Keeps server host string. */
    private static final String LOCALHOST = "localhost";

    /** Keeps length of url. */
    private int length = 0;

    /** Master parameter name. */
    private static final String MASTER = ";master=";
    /** Encrypt parameter name. */
    private static final String ENCRYPT = ";encrypt=";
    /** Authenticate parameter name. */
    private static final String AUTHENTICATE = ";authenticate=";
    /** Authorize parameter name. */
    private static final String AUTHORIZE = ";authorize=";
    /** TransmitMTU parameter name. */
    private static final String TRANSMITMTU = ";transmitmtu=";
    /** ReceiveMTU parameter name. */
    private static final String RECEIVEMTU = ";receivemtu=";
    /** Name parameter name. */
    private static final String NAME = ";name=";

    /** "true" literal. */
    private static final String TRUE = "true";
    /** "false" literal. */
    private static final String FALSE = "false";

    /** the URL parameters. */
    private Hashtable parameters;

    /** Stub object for values in parameters hashtable.*/
    private final static Object on = new Object();

    /** Shows whether this url is generated and validated by SDP routines. */
    private boolean isSystem = false;

    /**
     * Constructs url object by specified url string. Constructing
     * <code>BluetoothUrl</code> in this manner is a way to parse
     * an url represented by string.
     *
     * @param urlString url string.
     */
    public BluetoothUrl(String urlString) {
        this(UNKNOWN, urlString, null);
    }

    /**
     * Constructs url object by specified protocol and url string without
     * leading protocol name and colon or if protocol is unknown by s string
     * that contains full url.
     *
     * @param protocol prootocol type, must be one of
     *        <code>L2CAP, RFCOMM, OBEX, UNKNOWN</code>.
     * @param urlString whole url if <code>protocol</code> value is
     *        <code>UNKNOWN</code>, a part of url string beyond
     *        "protocol:" otherwise.
     */
    public BluetoothUrl(int protocol, String urlString) {
        this(protocol, urlString, null);
    }

    /**
     * Constructs url object with specified protocol, url and special system
     * token.
     * @see BluetoothUrl(int, String)
     *
     * @param protocol prootocol type
     * @param urlString URL
     * @param systemToken special object that validates this URL as system
     *        if has proper value, usually it is <code>null</code>
     */
    public BluetoothUrl(int protocol, String urlString, Object systemToken) {
        this(protocol);

        isSystem = SDP.checkSystemToken(systemToken);
        caseSensitiveUrl = urlString;
        url = urlString.toLowerCase();
        length = url.length();
        int start;
        int separator = url.indexOf(':');

        if (protocol == UNKNOWN) {
            // url is "PROTOCOL://ADDRESS:...", parsing protocol name
            assertTrue(separator > 0, "Cannot parse protocol name: " + url);
            start = separator + 3; // skip "://"
            String name = urlString.substring(0, start);

            for (int i = 0; i < PROTOCOLS_AMOUNT; i++) {
                if (protocolName[i].equals(name)) {
                    this.protocol = i;
                    separator = url.indexOf(':', start);
                    break;
                }
            }

        } else {
            // url is "//ADDRESS:...", parsing protocol name
            assertTrue(urlString.startsWith("//"),
            "address and protocol name have to be separated by //: " + url);
            // skip "//"
            start = 2;
        }

        assertTrue(separator > start, "Cannot parse address: " + url);

        address = url.substring(start, separator);
        start = separator + 1;


        if (this.protocol == L2CAP) {
            // parsing psm or uuid
            if (address.equals(LOCALHOST)) {
                isServer = true;
                // Now uuid goes till end of string or semicolon.
                separator = getSeparator(start);
                uuid = url.substring(start, separator);

             } else {
                // Now psm goes which is represented by 4 hex digits.
                assertTrue((separator = start + 4) <= length,
                "psm has to be represented by 4 hex digits: " + url);
                port = parseInt(start, separator, 16);
             }

        } else if (this.protocol == RFCOMM ||
                   this.protocol == OBEX) {
            separator = getSeparator(start);
            if (address.equals(LOCALHOST)) {
                isServer = true;
                // Now uuid goes till end of string or semicolon.
                uuid = url.substring(start, separator);
            } else {
                // Now channel id goes which is represented by %d1-30.
                assertTrue(separator <= length,
                "channel id has to go after address: " + url);
                port = parseInt(start, separator, 10);
            }
        } else {
            separator = getSeparator(start);
            port = parseInt(start, separator, 16);
        }

        if (isServer) {
            int length;
            assertTrue(uuid != null && (length = uuid.length()) > 0 &&
                length <= 32, "Invalid UUID");
        } else {
            checkBluetoothAddress();
        }

        // parsing parameters
        parameters = new Hashtable();
        for (start = separator; start < length; start = parseParameter(start));
        parameters = null;

        assertTrue(start == length, "Cannot parse the parameters: " + url);
    }

    /**
     * Creates url that represents client connection string.
     *
     * @param protocol identifies protocol. Should be one of
     * <code>
     * BluetoothUrl.L2CAP, BluetoothUrl.RFCOMM, BluetoothUrl.OBEX
     * </code>
     *
     * @param btaddr Bluetooth address of server device.
     *
     * @param port PSM in case of L2CAP or channel id otherwise.
     *
     * @return <code>BluetoothUrl</code> instance that represents
     * desired connection string.
     *
     * @exception IllegalArgument exception if provived parameters are invalid.
     */
    public static BluetoothUrl createClientUrl(int protocol,
                                               String btaddr, int port)
            throws IllegalArgumentException {

        assertTrue(protocol != UNKNOWN && btaddr != null,
        "Either unknown protocol name or address");
        BluetoothUrl url = new BluetoothUrl(protocol);

        url.address = btaddr.toLowerCase();
        url.checkBluetoothAddress();
        url.port = port;

        return url;
    }

    /**
     * Universal private constructor.
     * @param protocol identifies protocol.
     * @exception IllegalArgument exception if provived parameters are invalid.
     */
    private BluetoothUrl(int protocol) {
        assertTrue(protocol <= UNKNOWN, "Unknown protocol name: " + protocol);
        this.protocol = protocol;
    }

    /**
     * Checks url parts consistency and creates string representation.
     * @return string representation of the URL.
     * @exception IllegalArgumentException if URL parts are inconsistent.
     */
    public String toString() {
        assertTrue(protocol == L2CAP ||
                   protocol == RFCOMM ||
                   protocol == OBEX,
                   "Incorrect protocol bname: " + protocol);

        StringBuffer buffer = new StringBuffer();

        buffer = new StringBuffer(getResourceName());
        buffer.append(':');

        if (isServer) {
            buffer.append(uuid);
            buffer.append(AUTHORIZE).append(authorize ? TRUE : FALSE);
        } else {
            String portStr;

            if (protocol == L2CAP) {
                // in case of l2cap, the psm is 4 hex digits
                portStr = Integer.toHexString(port);
                for (int pad = 4 - portStr.length(); pad > 0; pad--) {
                    buffer.append('0');
                }

            } else if (protocol == RFCOMM ||
                       protocol == OBEX) {
                portStr = Integer.toString(port);
            } else {
                portStr = Integer.toString(port);
            }

            buffer.append(portStr);
        }

        /*
         * note: actually it's not required to add the boolean parameter if it
         * equals to false because if it is not present in the connection
         * string, this is equivalent to 'such parameter'=false.
         * But some TCK tests check the parameter is always present in
         * URL string even its value is false.
         * IMPL_NOTE: revisit this code if TCK changes.
         */
        buffer.append(MASTER).append(master ? TRUE : FALSE);
        buffer.append(ENCRYPT).append(encrypt ? TRUE: FALSE);
        buffer.append(AUTHENTICATE).append(authenticate ? TRUE : FALSE);

        if (receiveMTU != -1) {
            buffer.append(RECEIVEMTU).append(
                Integer.toString(receiveMTU, 10));
        }
        if (transmitMTU != -1) {
            buffer.append(TRANSMITMTU).append(
                Integer.toString(transmitMTU, 10));
        }

        return buffer.toString();
    }

    /**
     * Creates string representation of the URL without parameters.
     * @return "PROTOCOL://ADDRESS" string.
     */
    public String getResourceName() {
        assertTrue(protocol == L2CAP ||
                   protocol == RFCOMM ||
                   protocol == OBEX,
                   "Incorrect protocol bname: " + protocol);
        assertTrue(address != null, "Incorrect address: "+ address);
        return protocolName[protocol] + address;
    }

    /**
     * Tests if this URL is system one. System URL can only by created
     * by SDP server or client and is processed in special way.
     *
     * @return <code>true</code> if this url is a system one created
     *         by SDP routines, <code>false</code> otherwise
     */
    public final boolean isSystem() {
        return isSystem;
    }

    /**
     * Checks the string given is a valid Bluetooth address, which means
     * consists of 12 hexadecimal digits.
     *
     * @exception IllegalArgumentException if string given is not a valid
     *             Bluetooth address
     */
    private void checkBluetoothAddress()
            throws IllegalArgumentException {

        String errorMessage = "Invalid Bluetooth address";
        assertTrue(address != null && address.length() == 12 &&
                address.indexOf('-') == -1, errorMessage);

        try {
            Long.parseLong(address, 16);
        } catch (NumberFormatException e) {
            assertTrue(false, errorMessage);
        }
    }

    /**
     * Parses parameter in url starting at given position and cheks simple
     * rules or parameters compatibility. Parameter is ";NAME=VALUE". If
     * parsing from given position or a check fails,
     * <code>IllegalArgumentException</code> is thrown.
     *
     * @param start position to start parsing at, if it does not point to
     *        semicolon, parsing fails as well as instance constructing.
     * @return position number that immediately follows parsed parameter.
     * @exception IllegalArgumentException if parsing fails or incompatible
     *        parameters occured.
     */
    private int parseParameter(int start) throws IllegalArgumentException {
        assertTrue(url.charAt(start) == ';',
                   "Cannot parse url parameters: " + url);

        int separator = url.indexOf('=', start) + 1;
        assertTrue(separator > 0, "Cannot parse url parameters: " + url);
        // name is ";NAME="
        String name = url.substring(start, separator);

        start = separator;
        separator = getSeparator(start);

        assertTrue(!parameters.containsKey(name),
        "Duplicate parameter " + name);
        parameters.put(name, on);

        if (name.equals(MASTER)) {
            master = parseBoolean(start, separator);

        } else if (name.equals(ENCRYPT)) {
            encrypt = parseBoolean(start, separator);
            if (encrypt && !explicitAuthenticate) {
                authenticate = true;
            }

        } else if (name.equals(AUTHENTICATE)) {
            authenticate = parseBoolean(start, separator);
            explicitAuthenticate = true;

        } else if (name.equals(NAME)) {
            assertTrue(isServer, "Incorrect parameter for client: " + name);
            // this parameter is case-sensitive
            this.name = caseSensitiveUrl.substring(start, separator);
            assertTrue(checkNameFormat(this.name),
            "Incorrect name format: " + this.name);

        } else if (name.equals(AUTHORIZE)) {
            assertTrue(isServer,  "Incorrect parameter for client: " + name);
            authorize = parseBoolean(start, separator);
            if (authorize && !explicitAuthenticate) {
                authenticate = true;
            }

        } else if (protocol == L2CAP) {
            if (name.equals(RECEIVEMTU)) {
                receiveMTU = parseInt(start, separator, 10);
                assertTrue(receiveMTU > 0,
                           "Incorrect receive MTU: " + receiveMTU);
            } else if (name.equals(TRANSMITMTU)) {
                transmitMTU = parseInt(start, separator, 10);
                assertTrue(transmitMTU > 0,
                           "Incorrect transmit MTU: " + transmitMTU);
            } else {
                assertTrue(false, "Unknown parameter name = " + name);
            }
        } else {
            assertTrue(false, "Unknown parameter name = " + name);
        }
        return separator;
    }

    /**
     * Checks name format.
     * name = 1*( ALPHA / DIGIT / SP / "-" / "_")
     * The core rules from RFC 2234.
     *
     * @param name the name
     * @return <code>true</code> if the name format is valid,
     *         <code>false</code> otherwise
     */
    private boolean checkNameFormat(String name) {
        char[] a = name.toCharArray();
        boolean ret = a.length > 0;
        for (int i = a.length; --i >= 0 && ret;) {
            ret &= (a[i] >= 'a' && a[i] <= 'z') ||
                (a[i] >= 'A' && a[i] <= 'Z') ||
                (a[i] >= '0' && a[i] <= '9') ||
                (a[i] == '-') ||
                (a[i] == '_') ||
                (a[i] == ' ');
        }
        return ret;
    }

    /**
     * Retrieves position of semicolon in the rest of url string, returning
     * length of the string if no semicolon found. It also assertd that a
     * non-empty substring starts from <code>start</code> given and ends
     * before semicolon or end of string.
     *
     * @param start position in url string to start searching at.
     *
     * @return position of first semicolon or length of url string if there
     * is no semicolon.
     *
     * @exception IllegalArgumentException if there is no non-empty substring
     * before semicolon or end of url.
     */
    private int getSeparator(int start) {
        int separator = url.indexOf(';', start);
        if (separator < 0) {
            separator = length;
        }
        assertTrue(start < separator, "Correct separator is not found");

        return separator;
    }

    /**
     * Parses boolean value from url string.
     *
     * @param start position to start parsing from.
     * @param separator position that immediately follows value to parse.
     *
     * @return true if, comparing case insensitive, specified substring is
     *        "TRUE", false if it is "FALSE".
     * @exception IllegalArgumentException if specified url substring is
     *        neither "TRUE" nor "FALSE", case-insensitive.
     */
    private boolean parseBoolean(int start, int separator)
            throws IllegalArgumentException {
        String value = url.substring(start, separator);
        if (value.equals(TRUE)) {
            return true;
        }

        assertTrue(value.equals(FALSE), "Incorrect boolean parsing: " + value);
        return false;
    }

    /**
     * Parses integer value from url string.
     *
     * @param start position to start parsing from.
     * @param separator position that immediately follows value to parse.
     * @param radix the radix to use.
     *
     * @return integer value been parsed.
     * @exception IllegalArgumentException if given string is not
     *        case-insensitive "TRUE" or "FALSE".
     */
    private int parseInt(int start, int separator, int radix)
            throws IllegalArgumentException {
        int result = -1;

        try {
            result = Integer.parseInt(
                url.substring(start, separator), radix);
        } catch (NumberFormatException e) {
            assertTrue(false, "Incorrect int parsing: " +
                       url.substring(start, separator));
        }

        return result;
    }

    /**
     * Asserts that given condition is true.
     * @param condition condition to check.
     * @param details condition's description details.
     * @exception IllegalArgumentException if given condition is flase.
     */
    private static void assertTrue(boolean condition, String details)
            throws IllegalArgumentException {
        if (!condition) {
            throw new IllegalArgumentException("unexpected parameter: " +
                                               details);
        }
    }
}