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