/*
* Conditions Of Use
*
* This software was developed by employees of the National Institute of
* Standards and Technology (NIST), an agency of the Federal Government.
* Pursuant to title 15 Untied States Code Section 105, works of NIST
* employees are not subject to copyright protection in the United States
* and are considered to be in the public domain. As a result, a formal
* license is not needed to use the software.
*
* This software is provided by NIST as a service and is expressly
* provided "AS IS." NIST MAKES NO WARRANTY OF ANY KIND, EXPRESS, IMPLIED
* OR STATUTORY, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTY OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT
* AND DATA ACCURACY. NIST does not warrant or make any representations
* regarding the use of the software or the results thereof, including but
* not limited to the correctness, accuracy, reliability or usefulness of
* the software.
*
* Permission to use this software is contingent upon your acceptance
* of the terms of this agreement
*
* .
*
*/
package gov.nist.javax.sip.parser;
import gov.nist.core.HostNameParser;
import gov.nist.core.HostPort;
import gov.nist.core.NameValue;
import gov.nist.core.NameValueList;
import gov.nist.core.Token;
import gov.nist.javax.sip.address.GenericURI;
import gov.nist.javax.sip.address.SipUri;
import gov.nist.javax.sip.address.TelURLImpl;
import gov.nist.javax.sip.address.TelephoneNumber;
import java.text.ParseException;
/**
* Parser For SIP and Tel URLs. Other kinds of URL's are handled by the
* J2SE 1.4 URL class.
* @version 1.2 $Revision: 1.27 $ $Date: 2009/10/22 10:27:39 $
*
* @author M. Ranganathan <br/>
*
*
*/
public class URLParser extends Parser {
public URLParser(String url) {
this.lexer = new Lexer("sip_urlLexer", url);
}
// public tag added - issued by Miguel Freitas
public URLParser(Lexer lexer) {
this.lexer = lexer;
this.lexer.selectLexer("sip_urlLexer");
}
protected static boolean isMark(char next) {
switch (next) {
case '-':
case '_':
case '.':
case '!':
case '~':
case '*':
case '\'':
case '(':
case ')':
return true;
default:
return false;
}
}
protected static boolean isUnreserved(char next) {
return Lexer.isAlphaDigit(next) || isMark(next);
}
protected static boolean isReservedNoSlash(char next) {
switch (next) {
case ';':
case '?':
case ':':
case '@':
case '&':
case '+':
case '$':
case ',':
return true;
default:
return false;
}
}
// Missing '=' bug in character set - discovered by interop testing
// at SIPIT 13 by Bob Johnson and Scott Holben.
// change . to ; by Bruno Konik
protected static boolean isUserUnreserved(char la) {
switch (la) {
case '&':
case '?':
case '+':
case '$':
case '#':
case '/':
case ',':
case ';':
case '=':
return true;
default:
return false;
}
}
protected String unreserved() throws ParseException {
char next = lexer.lookAhead(0);
if (isUnreserved(next)) {
lexer.consume(1);
return String.valueOf(next);
} else
throw createParseException("unreserved");
}
/** Name or value of a parameter.
*/
protected String paramNameOrValue() throws ParseException {
int startIdx = lexer.getPtr();
while (lexer.hasMoreChars()) {
char next = lexer.lookAhead(0);
boolean isValidChar = false;
switch (next) {
case '[':
case ']':// JvB: fixed this one
case '/':
case ':':
case '&':
case '+':
case '$':
isValidChar = true;
}
if (isValidChar || isUnreserved(next)) {
lexer.consume(1);
} else if (isEscaped()) {
lexer.consume(3);
} else
break;
}
return lexer.getBuffer().substring(startIdx, lexer.getPtr());
}
private NameValue uriParam() throws ParseException {
if (debug)
dbg_enter("uriParam");
try {
String pvalue = "";
String pname = paramNameOrValue();
char next = lexer.lookAhead(0);
boolean isFlagParam = true;
if (next == '=') {
lexer.consume(1);
pvalue = paramNameOrValue();
isFlagParam = false;
}
if (pname.length() == 0 &&
( pvalue == null ||
pvalue.length() == 0))
return null;
else return new NameValue(pname, pvalue, isFlagParam);
} finally {
if (debug)
dbg_leave("uriParam");
}
}
protected static boolean isReserved(char next) {
switch (next) {
case ';':
case '/':
case '?':
case ':':
case '=': // Bug fix by Bruno Konik
case '@':
case '&':
case '+':
case '$':
case ',':
return true;
default:
return false;
}
}
protected String reserved() throws ParseException {
char next = lexer.lookAhead(0);
if (isReserved(next)) {
lexer.consume(1);
return new StringBuffer().append(next).toString();
} else
throw createParseException("reserved");
}
protected boolean isEscaped() {
try {
return lexer.lookAhead(0) == '%' &&
Lexer.isHexDigit(lexer.lookAhead(1)) &&
Lexer.isHexDigit(lexer.lookAhead(2));
} catch (Exception ex) {
return false;
}
}
protected String escaped() throws ParseException {
if (debug)
dbg_enter("escaped");
try {
StringBuffer retval = new StringBuffer();
char next = lexer.lookAhead(0);
char next1 = lexer.lookAhead(1);
char next2 = lexer.lookAhead(2);
if (next == '%'
&& Lexer.isHexDigit(next1)
&& Lexer.isHexDigit(next2)) {
lexer.consume(3);
retval.append(next);
retval.append(next1);
retval.append(next2);
} else
throw createParseException("escaped");
return retval.toString();
} finally {
if (debug)
dbg_leave("escaped");
}
}
protected String mark() throws ParseException {
if (debug)
dbg_enter("mark");
try {
char next = lexer.lookAhead(0);
if (isMark(next)) {
lexer.consume(1);
return new String( new char[]{next} );
} else
throw createParseException("mark");
} finally {
if (debug)
dbg_leave("mark");
}
}
protected String uric() {
if (debug)
dbg_enter("uric");
try {
try {
char la = lexer.lookAhead(0);
if (isUnreserved(la)) {
lexer.consume(1);
return Lexer.charAsString(la);
} else if (isReserved(la)) {
lexer.consume(1);
return Lexer.charAsString(la);
} else if (isEscaped()) {
String retval = lexer.charAsString(3);
lexer.consume(3);
return retval;
} else
return null;
} catch (Exception ex) {
return null;
}
} finally {
if (debug)
dbg_leave("uric");
}
}
protected String uricNoSlash() {
if (debug)
dbg_enter("uricNoSlash");
try {
try {
char la = lexer.lookAhead(0);
if (isEscaped()) {
String retval = lexer.charAsString(3);
lexer.consume(3);
return retval;
} else if (isUnreserved(la)) {
lexer.consume(1);
return Lexer.charAsString(la);
} else if (isReservedNoSlash(la)) {
lexer.consume(1);
return Lexer.charAsString(la);
} else
return null;
} catch (ParseException ex) {
return null;
}
} finally {
if (debug)
dbg_leave("uricNoSlash");
}
}
protected String uricString() throws ParseException {
StringBuffer retval = new StringBuffer();
while (true) {
String next = uric();
if (next == null) {
char la = lexer.lookAhead(0);
// JvB: allow IPv6 addresses in generic URI strings
// e.g. http://[::1]
if ( la == '[' ) {
HostNameParser hnp = new HostNameParser(this.getLexer());
HostPort hp = hnp.hostPort( false );
retval.append(hp.toString());
continue;
}
break;
}
retval.append(next);
}
return retval.toString();
}
/**
* Parse and return a structure for a generic URL.
* Note that non SIP URLs are just stored as a string (not parsed).
* @return URI is a URL structure for a SIP url.
* @throws ParseException if there was a problem parsing.
*/
public GenericURI uriReference( boolean inBrackets ) throws ParseException {
if (debug)
dbg_enter("uriReference");
GenericURI retval = null;
Token[] tokens = lexer.peekNextToken(2);
Token t1 = (Token) tokens[0];
Token t2 = (Token) tokens[1];
try {
if (t1.getTokenType() == TokenTypes.SIP ||
t1.getTokenType() == TokenTypes.SIPS) {
if (t2.getTokenType() == ':')
retval = sipURL( inBrackets );
else
throw createParseException("Expecting \':\'");
} else if (t1.getTokenType() == TokenTypes.TEL) {
if (t2.getTokenType() == ':') {
retval = telURL( inBrackets );
} else
throw createParseException("Expecting \':\'");
} else {
String urlString = uricString();
try {
retval = new GenericURI(urlString);
} catch (ParseException ex) {
throw createParseException(ex.getMessage());
}
}
} finally {
if (debug)
dbg_leave("uriReference");
}
return retval;
}
/**
* Parser for the base phone number.
*/
private String base_phone_number() throws ParseException {
StringBuffer s = new StringBuffer();
if (debug)
dbg_enter("base_phone_number");
try {
int lc = 0;
while (lexer.hasMoreChars()) {
char w = lexer.lookAhead(0);
if (Lexer.isDigit(w)
|| w == '-'
|| w == '.'
|| w == '('
|| w == ')') {
lexer.consume(1);
s.append(w);
lc++;
} else if (lc > 0)
break;
else
throw createParseException("unexpected " + w);
}
return s.toString();
} finally {
if (debug)
dbg_leave("base_phone_number");
}
}
/**
* Parser for the local phone #.
*/
private String local_number() throws ParseException {
StringBuffer s = new StringBuffer();
if (debug)
dbg_enter("local_number");
try {
int lc = 0;
while (lexer.hasMoreChars()) {
char la = lexer.lookAhead(0);
if (la == '*'
|| la == '#'
|| la == '-'
|| la == '.'
|| la == '('
|| la == ')'
// JvB: allow 'A'..'F', should be uppercase
|| Lexer.isHexDigit(la)) {
lexer.consume(1);
s.append(la);
lc++;
} else if (lc > 0)
break;
else
throw createParseException("unexepcted " + la);
}
return s.toString();
} finally {
if (debug)
dbg_leave("local_number");
}
}
/**
* Parser for telephone subscriber.
*
* @return the parsed telephone number.
*/
public final TelephoneNumber parseTelephoneNumber( boolean inBrackets )
throws ParseException {
TelephoneNumber tn;
if (debug)
dbg_enter("telephone_subscriber");
lexer.selectLexer("charLexer");
try {
char c = lexer.lookAhead(0);
if (c == '+')
tn = global_phone_number( inBrackets );
else if (
Lexer.isHexDigit(c)// see RFC3966
|| c == '#'
|| c == '*'
|| c == '-'
|| c == '.'
|| c == '('
|| c == ')' ) {
tn = local_phone_number( inBrackets );
} else
throw createParseException("unexpected char " + c);
return tn;
} finally {
if (debug)
dbg_leave("telephone_subscriber");
}
}
private final TelephoneNumber global_phone_number( boolean inBrackets ) throws ParseException {
if (debug)
dbg_enter("global_phone_number");
try {
TelephoneNumber tn = new TelephoneNumber();
tn.setGlobal(true);
NameValueList nv = null;
this.lexer.match(PLUS);
String b = base_phone_number();
tn.setPhoneNumber(b);
if (lexer.hasMoreChars()) {
char tok = lexer.lookAhead(0);
if (tok == ';' && inBrackets) {
this.lexer.consume(1);
nv = tel_parameters();
tn.setParameters(nv);
}
}
return tn;
} finally {
if (debug)
dbg_leave("global_phone_number");
}
}
private TelephoneNumber local_phone_number( boolean inBrackets ) throws ParseException {
if (debug)
dbg_enter("local_phone_number");
TelephoneNumber tn = new TelephoneNumber();
tn.setGlobal(false);
NameValueList nv = null;
String b = null;
try {
b = local_number();
tn.setPhoneNumber(b);
if (lexer.hasMoreChars()) {
Token tok = this.lexer.peekNextToken();
switch (tok.getTokenType()) {
case SEMICOLON:
{
if (inBrackets) {
this.lexer.consume(1);
nv = tel_parameters();
tn.setParameters(nv);
}
break;
}
default :
{
break;
}
}
}
} finally {
if (debug)
dbg_leave("local_phone_number");
}
return tn;
}
private NameValueList tel_parameters() throws ParseException {
NameValueList nvList = new NameValueList();
// JvB: Need to handle 'phone-context' specially
// 'isub' (or 'ext') MUST appear first, but we accept any order here
NameValue nv;
while ( true ) {
String pname = paramNameOrValue();
// Handle 'phone-context' specially, it may start with '+'
if ( pname.equalsIgnoreCase("phone-context")) {
nv = phone_context();
} else {
if (lexer.lookAhead(0) == '=') {
lexer.consume(1);
String value = paramNameOrValue();
nv = new NameValue( pname, value, false );
} else {
nv = new NameValue( pname, "", true );// flag param
}
}
nvList.set( nv );
if ( lexer.lookAhead(0) == ';' ) {
lexer.consume(1);
} else {
return nvList;
}
}
}
/**
* Parses the 'phone-context' parameter in tel: URLs
* @throws ParseException
*/
private NameValue phone_context() throws ParseException {
lexer.match('=');
char la = lexer.lookAhead(0);
Object value;
if (la=='+') {// global-number-digits
lexer.consume(1);// skip '+'
value = "+" + base_phone_number();
} else if ( Lexer.isAlphaDigit(la) ) {
Token t = lexer.match( Lexer.ID );// more broad than allowed
value = t.getTokenValue();
} else {
throw new ParseException( "Invalid phone-context:" + la , -1 );
}
return new NameValue( "phone-context", value, false );
}
/**
* Parse and return a structure for a Tel URL.
* @return a parsed tel url structure.
*/
public TelURLImpl telURL( boolean inBrackets ) throws ParseException {
lexer.match(TokenTypes.TEL);
lexer.match(':');
TelephoneNumber tn = this.parseTelephoneNumber(inBrackets);
TelURLImpl telUrl = new TelURLImpl();
telUrl.setTelephoneNumber(tn);
return telUrl;
}
/**
* Parse and return a structure for a SIP URL.
* @return a URL structure for a SIP url.
* @throws ParseException if there was a problem parsing.
*/
public SipUri sipURL( boolean inBrackets ) throws ParseException {
if (debug)
dbg_enter("sipURL");
SipUri retval = new SipUri();
// pmusgrave - handle sips case
Token nextToken = lexer.peekNextToken();
int sipOrSips = TokenTypes.SIP;
String scheme = TokenNames.SIP;
if ( nextToken.getTokenType() == TokenTypes.SIPS)
{
sipOrSips = TokenTypes.SIPS;
scheme = TokenNames.SIPS;
}
try {
lexer.match(sipOrSips);
lexer.match(':');
retval.setScheme(scheme);
int startOfUser = lexer.markInputPosition();
String userOrHost = user();// Note: user may contain ';', host may not...
String passOrPort = null;
// name:password or host:port
if ( lexer.lookAhead() == ':' ) {
lexer.consume(1);
passOrPort = password();
}
// name@hostPort
if ( lexer.lookAhead() == '@' ) {
lexer.consume(1);
retval.setUser( userOrHost );
if (passOrPort!=null) retval.setUserPassword( passOrPort );
} else {
// then userOrHost was a host, backtrack just in case a ';' was eaten...
lexer.rewindInputPosition( startOfUser );
}
HostNameParser hnp = new HostNameParser(this.getLexer());
HostPort hp = hnp.hostPort( false );
retval.setHostPort(hp);
lexer.selectLexer("charLexer");
while (lexer.hasMoreChars()) {
// If the URI is not enclosed in brackets, parameters belong to header
if (lexer.lookAhead(0) != ';' || !inBrackets)
break;
lexer.consume(1);
NameValue parms = uriParam();
if (parms != null) retval.setUriParameter(parms);
}
if (lexer.hasMoreChars() && lexer.lookAhead(0) == '?') {
lexer.consume(1);
while (lexer.hasMoreChars()) {
NameValue parms = qheader();
retval.setQHeader(parms);
if (lexer.hasMoreChars() && lexer.lookAhead(0) != '&')
break;
else
lexer.consume(1);
}
}
return retval;
// BEGIN android-added
} catch (RuntimeException e) {
throw new ParseException("Invalid URL: " + lexer.getBuffer(), -1);
// END android-added
} finally {
if (debug)
dbg_leave("sipURL");
}
}
public String peekScheme() throws ParseException {
Token[] tokens = lexer.peekNextToken(1);
if (tokens.length == 0)
return null;
String scheme = ((Token) tokens[0]).getTokenValue();
return scheme;
}
/**
* Get a name value for a given query header (ie one that comes
* after the ?).
*/
protected NameValue qheader() throws ParseException {
String name = lexer.getNextToken('=');
lexer.consume(1);
String value = hvalue();
return new NameValue(name, value, false);
}
protected String hvalue() throws ParseException {
StringBuffer retval = new StringBuffer();
while (lexer.hasMoreChars()) {
char la = lexer.lookAhead(0);
// Look for a character that can terminate a URL.
boolean isValidChar = false;
switch (la) {
case '+':
case '?':
case ':':
case '[':
case ']':
case '/':
case '$':
case '_':
case '-':
case '"':
case '!':
case '~':
case '*':
case '.':
case '(':
case ')':
isValidChar = true;
}
if (isValidChar || Lexer.isAlphaDigit(la)) {
lexer.consume(1);
retval.append(la);
} else if (la == '%') {
retval.append(escaped());
} else
break;
}
return retval.toString();
}
/**
* Scan forward until you hit a terminating character for a URL.
* We do not handle non sip urls in this implementation.
* @return the string that takes us to the end of this URL (i.e. to
* the next delimiter).
*/
protected String urlString() throws ParseException {
StringBuffer retval = new StringBuffer();
lexer.selectLexer("charLexer");
while (lexer.hasMoreChars()) {
char la = lexer.lookAhead(0);
// Look for a character that can terminate a URL.
if (la == ' '
|| la == '\t'
|| la == '\n'
|| la == '>'
|| la == '<')
break;
lexer.consume(0);
retval.append(la);
}
return retval.toString();
}
protected String user() throws ParseException {
if (debug)
dbg_enter("user");
try {
int startIdx = lexer.getPtr();
while (lexer.hasMoreChars()) {
char la = lexer.lookAhead(0);
if (isUnreserved(la) || isUserUnreserved(la)) {
lexer.consume(1);
} else if (isEscaped()) {
lexer.consume(3);
} else
break;
}
return lexer.getBuffer().substring(startIdx, lexer.getPtr());
} finally {
if (debug)
dbg_leave("user");
}
}
protected String password() throws ParseException {
int startIdx = lexer.getPtr();
while (true) {
char la = lexer.lookAhead(0);
boolean isValidChar = false;
switch (la) {
case '&':
case '=':
case '+':
case '$':
case ',':
isValidChar = true;
}
if (isValidChar || isUnreserved(la)) {
lexer.consume(1);
} else if (isEscaped()) {
lexer.consume(3); // bug reported by
// Jeff Haynie
} else
break;
}
return lexer.getBuffer().substring(startIdx, lexer.getPtr());
}
/**
* Default parse method. This method just calls uriReference.
*/
public GenericURI parse() throws ParseException {
return uriReference( true );
}
// quick test routine for debugging type assignment
public static void main(String[] args) throws ParseException
{
// quick test for sips parsing
String[] test = { "sip:alice@example.com",
"sips:alice@examples.com" ,
"sip:3Zqkv5dajqaaas0tCjCxT0xH2ZEuEMsFl0xoasip%3A%2B3519116786244%40siplab.domain.com@213.0.115.163:7070"};
for ( int i = 0; i < test.length; i++)
{
URLParser p = new URLParser(test[i]);
GenericURI uri = p.parse();
System.out.println("uri type returned " + uri.getClass().getName());
System.out.println(test[i] + " is SipUri? " + uri.isSipURI()
+ ">" + uri.encode());
}
}
/**
**/
}