001: /*
002: *
003: *
004: * Copyright 1990-2007 Sun Microsystems, Inc. All Rights Reserved.
005: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
006: *
007: * This program is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU General Public License version
009: * 2 only, as published by the Free Software Foundation.
010: *
011: * This program is distributed in the hope that it will be useful, but
012: * WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * General Public License version 2 for more details (a copy is
015: * included at /legal/license.txt).
016: *
017: * You should have received a copy of the GNU General Public License
018: * version 2 along with this work; if not, write to the Free Software
019: * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020: * 02110-1301 USA
021: *
022: * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
023: * Clara, CA 95054 or visit www.sun.com if you need additional
024: * information or have any questions.
025: */
026: package com.sun.midp.io;
027:
028: import javax.bluetooth.UUID;
029: import javax.bluetooth.BluetoothConnectionException;
030: import java.util.Hashtable;
031: import com.sun.kvem.jsr082.bluetooth.SDP;
032:
033: /**
034: * Represents a bluetooth url, i.e. connection string.
035: * There are two ways of usage. First one is constructing it giving
036: * url string in order to parse it into a set of fields. Second one
037: * is constructig it giving fields values in order to get string
038: * representation. Whenever incompatible url parts are found
039: * <code>IllegalArgumentException</code> is thrown.
040: */
041: public class BluetoothUrl {
042: /** Indicates if it is a sever connection string. */
043: public boolean isServer = false;
044:
045: /** Keeps server address for client url, "localhost" for server. */
046: public String address = null;
047: /** PSM for L2CAP or channel id for RFCOMM. */
048: public int port = -1;
049: /** Master parameter, true by default for server. */
050: public boolean master = false;
051: /** Encrypt parameter. */
052: public boolean encrypt = false;
053: /** Authenticate parameter. */
054: public boolean authenticate = false;
055:
056: /** Value to indicate L2CAP protocol. */
057: public static final int L2CAP = 0;
058: /** Value to indicate RFCOMM protocol. */
059: public static final int RFCOMM = 1;
060: /** Value to indicate OBEX protocol. */
061: public static final int OBEX = 2;
062: /** Value to indicate unknown protocol. */
063: public static final int UNKNOWN = 3;
064: /** Indicates protocol type. */
065: public int protocol = UNKNOWN;
066:
067: /** Amount of protocols supported. */
068: private static final int PROTOCOLS_AMOUNT = 3;
069: /**
070: * Keeps protocols indicating strings.
071: * Usage:
072: * <code>protocolName[L2CAP]</code> to get "l2cap"
073: */
074: private static final String[] protocolName = { "btl2cap://",
075: "btspp://", "btgoep://" };
076:
077: /**
078: * Keeps uuid from server connection string,
079: * <code>null</code> for client's one.
080: * L2CAP, RFCOMM specific.
081: */
082: public String uuid = null;
083:
084: /**
085: * Name parameter of server url, <code>null</code> for client's one.
086: * L2CAP, RFCOMM specific.
087: */
088: public String name = null;
089:
090: /** Url string to parse, lower case. */
091: private String url;
092: /**
093: * Url string to parse, original case.
094: * Required for correct "name" parameter parsing for it is case-sensitive.
095: */
096: public String caseSensitiveUrl;
097:
098: /** Authorize parameter. L2CAP specific. */
099: public boolean authorize = false;
100: /** RecieveMTU parameter. L2CAP specific. */
101: public int receiveMTU = -1;
102: /** TransmitMTU parameter. L2CAP specific. */
103: public int transmitMTU = -1;
104:
105: /** UUID value to create a transport for Service Discovery Protocol. */
106: public static final UUID UUID_SDP = new UUID(0x0001);
107:
108: /** Indicates if an explicit "authenticate" parameter found. */
109: private boolean explicitAuthenticate = false;
110:
111: /** Keeps server host string. */
112: private static final String LOCALHOST = "localhost";
113:
114: /** Keeps length of url. */
115: private int length = 0;
116:
117: /** Master parameter name. */
118: private static final String MASTER = ";master=";
119: /** Encrypt parameter name. */
120: private static final String ENCRYPT = ";encrypt=";
121: /** Authenticate parameter name. */
122: private static final String AUTHENTICATE = ";authenticate=";
123: /** Authorize parameter name. */
124: private static final String AUTHORIZE = ";authorize=";
125: /** TransmitMTU parameter name. */
126: private static final String TRANSMITMTU = ";transmitmtu=";
127: /** ReceiveMTU parameter name. */
128: private static final String RECEIVEMTU = ";receivemtu=";
129: /** Name parameter name. */
130: private static final String NAME = ";name=";
131:
132: /** "true" literal. */
133: private static final String TRUE = "true";
134: /** "false" literal. */
135: private static final String FALSE = "false";
136:
137: /** the URL parameters. */
138: private Hashtable parameters;
139:
140: /** Stub object for values in parameters hashtable.*/
141: private final static Object on = new Object();
142:
143: /** Shows whether this url is generated and validated by SDP routines. */
144: private boolean isSystem = false;
145:
146: /**
147: * Constructs url object by specified url string. Constructing
148: * <code>BluetoothUrl</code> in this manner is a way to parse
149: * an url represented by string.
150: *
151: * @param urlString url string.
152: */
153: public BluetoothUrl(String urlString) {
154: this (UNKNOWN, urlString, null);
155: }
156:
157: /**
158: * Constructs url object by specified protocol and url string without
159: * leading protocol name and colon or if protocol is unknown by s string
160: * that contains full url.
161: *
162: * @param protocol prootocol type, must be one of
163: * <code>L2CAP, RFCOMM, OBEX, UNKNOWN</code>.
164: * @param urlString whole url if <code>protocol</code> value is
165: * <code>UNKNOWN</code>, a part of url string beyond
166: * "protocol:" otherwise.
167: */
168: public BluetoothUrl(int protocol, String urlString) {
169: this (protocol, urlString, null);
170: }
171:
172: /**
173: * Constructs url object with specified protocol, url and special system
174: * token.
175: * @see BluetoothUrl(int, String)
176: *
177: * @param protocol prootocol type
178: * @param urlString URL
179: * @param systemToken special object that validates this URL as system
180: * if has proper value, usually it is <code>null</code>
181: */
182: public BluetoothUrl(int protocol, String urlString,
183: Object systemToken) {
184: this (protocol);
185:
186: isSystem = SDP.checkSystemToken(systemToken);
187: caseSensitiveUrl = urlString;
188: url = urlString.toLowerCase();
189: length = url.length();
190: int start;
191: int separator = url.indexOf(':');
192:
193: if (protocol == UNKNOWN) {
194: // url is "PROTOCOL://ADDRESS:...", parsing protocol name
195: assertTrue(separator > 0, "Cannot parse protocol name: "
196: + url);
197: start = separator + 3; // skip "://"
198: String name = urlString.substring(0, start);
199:
200: for (int i = 0; i < PROTOCOLS_AMOUNT; i++) {
201: if (protocolName[i].equals(name)) {
202: this .protocol = i;
203: separator = url.indexOf(':', start);
204: break;
205: }
206: }
207:
208: } else {
209: // url is "//ADDRESS:...", parsing protocol name
210: assertTrue(urlString.startsWith("//"),
211: "address and protocol name have to be separated by //: "
212: + url);
213: // skip "//"
214: start = 2;
215: }
216:
217: assertTrue(separator > start, "Cannot parse address: " + url);
218:
219: address = url.substring(start, separator);
220: start = separator + 1;
221:
222: if (this .protocol == L2CAP) {
223: // parsing psm or uuid
224: if (address.equals(LOCALHOST)) {
225: isServer = true;
226: // Now uuid goes till end of string or semicolon.
227: separator = getSeparator(start);
228: uuid = url.substring(start, separator);
229:
230: } else {
231: // Now psm goes which is represented by 4 hex digits.
232: assertTrue((separator = start + 4) <= length,
233: "psm has to be represented by 4 hex digits: "
234: + url);
235: port = parseInt(start, separator, 16);
236: }
237:
238: } else if (this .protocol == RFCOMM || this .protocol == OBEX) {
239: separator = getSeparator(start);
240: if (address.equals(LOCALHOST)) {
241: isServer = true;
242: // Now uuid goes till end of string or semicolon.
243: uuid = url.substring(start, separator);
244: } else {
245: // Now channel id goes which is represented by %d1-30.
246: assertTrue(separator <= length,
247: "channel id has to go after address: " + url);
248: port = parseInt(start, separator, 10);
249: }
250: } else {
251: separator = getSeparator(start);
252: port = parseInt(start, separator, 16);
253: }
254:
255: if (isServer) {
256: int length;
257: assertTrue(uuid != null && (length = uuid.length()) > 0
258: && length <= 32, "Invalid UUID");
259: } else {
260: checkBluetoothAddress();
261: }
262:
263: // parsing parameters
264: parameters = new Hashtable();
265: for (start = separator; start < length; start = parseParameter(start))
266: ;
267: parameters = null;
268:
269: assertTrue(start == length, "Cannot parse the parameters: "
270: + url);
271: }
272:
273: /**
274: * Creates url that represents client connection string.
275: *
276: * @param protocol identifies protocol. Should be one of
277: * <code>
278: * BluetoothUrl.L2CAP, BluetoothUrl.RFCOMM, BluetoothUrl.OBEX
279: * </code>
280: *
281: * @param btaddr Bluetooth address of server device.
282: *
283: * @param port PSM in case of L2CAP or channel id otherwise.
284: *
285: * @return <code>BluetoothUrl</code> instance that represents
286: * desired connection string.
287: *
288: * @exception IllegalArgument exception if provived parameters are invalid.
289: */
290: public static BluetoothUrl createClientUrl(int protocol,
291: String btaddr, int port) throws IllegalArgumentException {
292:
293: assertTrue(protocol != UNKNOWN && btaddr != null,
294: "Either unknown protocol name or address");
295: BluetoothUrl url = new BluetoothUrl(protocol);
296:
297: url.address = btaddr.toLowerCase();
298: url.checkBluetoothAddress();
299: url.port = port;
300:
301: return url;
302: }
303:
304: /**
305: * Universal private constructor.
306: * @param protocol identifies protocol.
307: * @exception IllegalArgument exception if provived parameters are invalid.
308: */
309: private BluetoothUrl(int protocol) {
310: assertTrue(protocol <= UNKNOWN, "Unknown protocol name: "
311: + protocol);
312: this .protocol = protocol;
313: }
314:
315: /**
316: * Checks url parts consistency and creates string representation.
317: * @return string representation of the URL.
318: * @exception IllegalArgumentException if URL parts are inconsistent.
319: */
320: public String toString() {
321: assertTrue(protocol == L2CAP || protocol == RFCOMM
322: || protocol == OBEX, "Incorrect protocol bname: "
323: + protocol);
324:
325: StringBuffer buffer = new StringBuffer();
326:
327: buffer = new StringBuffer(getResourceName());
328: buffer.append(':');
329:
330: if (isServer) {
331: buffer.append(uuid);
332: buffer.append(AUTHORIZE).append(authorize ? TRUE : FALSE);
333: } else {
334: String portStr;
335:
336: if (protocol == L2CAP) {
337: // in case of l2cap, the psm is 4 hex digits
338: portStr = Integer.toHexString(port);
339: for (int pad = 4 - portStr.length(); pad > 0; pad--) {
340: buffer.append('0');
341: }
342:
343: } else if (protocol == RFCOMM || protocol == OBEX) {
344: portStr = Integer.toString(port);
345: } else {
346: portStr = Integer.toString(port);
347: }
348:
349: buffer.append(portStr);
350: }
351:
352: /*
353: * note: actually it's not required to add the boolean parameter if it
354: * equals to false because if it is not present in the connection
355: * string, this is equivalent to 'such parameter'=false.
356: * But some TCK tests check the parameter is always present in
357: * URL string even its value is false.
358: * IMPL_NOTE: revisit this code if TCK changes.
359: */
360: buffer.append(MASTER).append(master ? TRUE : FALSE);
361: buffer.append(ENCRYPT).append(encrypt ? TRUE : FALSE);
362: buffer.append(AUTHENTICATE).append(authenticate ? TRUE : FALSE);
363:
364: if (receiveMTU != -1) {
365: buffer.append(RECEIVEMTU).append(
366: Integer.toString(receiveMTU, 10));
367: }
368: if (transmitMTU != -1) {
369: buffer.append(TRANSMITMTU).append(
370: Integer.toString(transmitMTU, 10));
371: }
372:
373: return buffer.toString();
374: }
375:
376: /**
377: * Creates string representation of the URL without parameters.
378: * @return "PROTOCOL://ADDRESS" string.
379: */
380: public String getResourceName() {
381: assertTrue(protocol == L2CAP || protocol == RFCOMM
382: || protocol == OBEX, "Incorrect protocol bname: "
383: + protocol);
384: assertTrue(address != null, "Incorrect address: " + address);
385: return protocolName[protocol] + address;
386: }
387:
388: /**
389: * Tests if this URL is system one. System URL can only by created
390: * by SDP server or client and is processed in special way.
391: *
392: * @return <code>true</code> if this url is a system one created
393: * by SDP routines, <code>false</code> otherwise
394: */
395: public final boolean isSystem() {
396: return isSystem;
397: }
398:
399: /**
400: * Checks the string given is a valid Bluetooth address, which means
401: * consists of 12 hexadecimal digits.
402: *
403: * @exception IllegalArgumentException if string given is not a valid
404: * Bluetooth address
405: */
406: private void checkBluetoothAddress()
407: throws IllegalArgumentException {
408:
409: String errorMessage = "Invalid Bluetooth address";
410: assertTrue(address != null && address.length() == 12
411: && address.indexOf('-') == -1, errorMessage);
412:
413: try {
414: Long.parseLong(address, 16);
415: } catch (NumberFormatException e) {
416: assertTrue(false, errorMessage);
417: }
418: }
419:
420: /**
421: * Parses parameter in url starting at given position and cheks simple
422: * rules or parameters compatibility. Parameter is ";NAME=VALUE". If
423: * parsing from given position or a check fails,
424: * <code>IllegalArgumentException</code> is thrown.
425: *
426: * @param start position to start parsing at, if it does not point to
427: * semicolon, parsing fails as well as instance constructing.
428: * @return position number that immediately follows parsed parameter.
429: * @exception IllegalArgumentException if parsing fails or incompatible
430: * parameters occured.
431: */
432: private int parseParameter(int start)
433: throws IllegalArgumentException {
434: assertTrue(url.charAt(start) == ';',
435: "Cannot parse url parameters: " + url);
436:
437: int separator = url.indexOf('=', start) + 1;
438: assertTrue(separator > 0, "Cannot parse url parameters: " + url);
439: // name is ";NAME="
440: String name = url.substring(start, separator);
441:
442: start = separator;
443: separator = getSeparator(start);
444:
445: assertTrue(!parameters.containsKey(name),
446: "Duplicate parameter " + name);
447: parameters.put(name, on);
448:
449: if (name.equals(MASTER)) {
450: master = parseBoolean(start, separator);
451:
452: } else if (name.equals(ENCRYPT)) {
453: encrypt = parseBoolean(start, separator);
454: if (encrypt && !explicitAuthenticate) {
455: authenticate = true;
456: }
457:
458: } else if (name.equals(AUTHENTICATE)) {
459: authenticate = parseBoolean(start, separator);
460: explicitAuthenticate = true;
461:
462: } else if (name.equals(NAME)) {
463: assertTrue(isServer, "Incorrect parameter for client: "
464: + name);
465: // this parameter is case-sensitive
466: this .name = caseSensitiveUrl.substring(start, separator);
467: assertTrue(checkNameFormat(this .name),
468: "Incorrect name format: " + this .name);
469:
470: } else if (name.equals(AUTHORIZE)) {
471: assertTrue(isServer, "Incorrect parameter for client: "
472: + name);
473: authorize = parseBoolean(start, separator);
474: if (authorize && !explicitAuthenticate) {
475: authenticate = true;
476: }
477:
478: } else if (protocol == L2CAP) {
479: if (name.equals(RECEIVEMTU)) {
480: receiveMTU = parseInt(start, separator, 10);
481: assertTrue(receiveMTU > 0, "Incorrect receive MTU: "
482: + receiveMTU);
483: } else if (name.equals(TRANSMITMTU)) {
484: transmitMTU = parseInt(start, separator, 10);
485: assertTrue(transmitMTU > 0, "Incorrect transmit MTU: "
486: + transmitMTU);
487: } else {
488: assertTrue(false, "Unknown parameter name = " + name);
489: }
490: } else {
491: assertTrue(false, "Unknown parameter name = " + name);
492: }
493: return separator;
494: }
495:
496: /**
497: * Checks name format.
498: * name = 1*( ALPHA / DIGIT / SP / "-" / "_")
499: * The core rules from RFC 2234.
500: *
501: * @param name the name
502: * @return <code>true</code> if the name format is valid,
503: * <code>false</code> otherwise
504: */
505: private boolean checkNameFormat(String name) {
506: char[] a = name.toCharArray();
507: boolean ret = a.length > 0;
508: for (int i = a.length; --i >= 0 && ret;) {
509: ret &= (a[i] >= 'a' && a[i] <= 'z')
510: || (a[i] >= 'A' && a[i] <= 'Z')
511: || (a[i] >= '0' && a[i] <= '9') || (a[i] == '-')
512: || (a[i] == '_') || (a[i] == ' ');
513: }
514: return ret;
515: }
516:
517: /**
518: * Retrieves position of semicolon in the rest of url string, returning
519: * length of the string if no semicolon found. It also assertd that a
520: * non-empty substring starts from <code>start</code> given and ends
521: * before semicolon or end of string.
522: *
523: * @param start position in url string to start searching at.
524: *
525: * @return position of first semicolon or length of url string if there
526: * is no semicolon.
527: *
528: * @exception IllegalArgumentException if there is no non-empty substring
529: * before semicolon or end of url.
530: */
531: private int getSeparator(int start) {
532: int separator = url.indexOf(';', start);
533: if (separator < 0) {
534: separator = length;
535: }
536: assertTrue(start < separator, "Correct separator is not found");
537:
538: return separator;
539: }
540:
541: /**
542: * Parses boolean value from url string.
543: *
544: * @param start position to start parsing from.
545: * @param separator position that immediately follows value to parse.
546: *
547: * @return true if, comparing case insensitive, specified substring is
548: * "TRUE", false if it is "FALSE".
549: * @exception IllegalArgumentException if specified url substring is
550: * neither "TRUE" nor "FALSE", case-insensitive.
551: */
552: private boolean parseBoolean(int start, int separator)
553: throws IllegalArgumentException {
554: String value = url.substring(start, separator);
555: if (value.equals(TRUE)) {
556: return true;
557: }
558:
559: assertTrue(value.equals(FALSE), "Incorrect boolean parsing: "
560: + value);
561: return false;
562: }
563:
564: /**
565: * Parses integer value from url string.
566: *
567: * @param start position to start parsing from.
568: * @param separator position that immediately follows value to parse.
569: * @param radix the radix to use.
570: *
571: * @return integer value been parsed.
572: * @exception IllegalArgumentException if given string is not
573: * case-insensitive "TRUE" or "FALSE".
574: */
575: private int parseInt(int start, int separator, int radix)
576: throws IllegalArgumentException {
577: int result = -1;
578:
579: try {
580: result = Integer.parseInt(url.substring(start, separator),
581: radix);
582: } catch (NumberFormatException e) {
583: assertTrue(false, "Incorrect int parsing: "
584: + url.substring(start, separator));
585: }
586:
587: return result;
588: }
589:
590: /**
591: * Asserts that given condition is true.
592: * @param condition condition to check.
593: * @param details condition's description details.
594: * @exception IllegalArgumentException if given condition is flase.
595: */
596: private static void assertTrue(boolean condition, String details)
597: throws IllegalArgumentException {
598: if (!condition) {
599: throw new IllegalArgumentException("unexpected parameter: "
600: + details);
601: }
602: }
603: }
|