blob: ccb07eefe8e7223462814ad78d7fb5f711d3e676 [file] [log] [blame]
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.qpid.messaging.util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.qpid.configuration.ClientProperties;
import org.apache.qpid.messaging.Address;
/**
* AddressParser
*
*/
public class AddressParser extends Parser
{
private static Lexicon lxi = new Lexicon();
private static Token.Type LBRACE = lxi.define("LBRACE", "\\{");
private static Token.Type RBRACE = lxi.define("RBRACE", "\\}");
private static Token.Type LBRACK = lxi.define("LBRACK", "\\[");
private static Token.Type RBRACK = lxi.define("RBRACK", "\\]");
private static Token.Type COLON = lxi.define("COLON", ":");
private static Token.Type SEMI = lxi.define("SEMI", ";");
private static Token.Type SLASH = lxi.define("SLASH", "/");
private static Token.Type COMMA = lxi.define("COMMA", ",");
private static Token.Type NUMBER = lxi.define("NUMBER", "[+-]?[0-9]*\\.?[0-9]+");
private static Token.Type TRUE = lxi.define("TRUE", "True");
private static Token.Type FALSE = lxi.define("FALSE", "False");
private static Token.Type ID = lxi.define("ID", "[a-zA-Z_](?:[a-zA-Z0-9_-]*[a-zA-Z0-9_])?");
private static Token.Type STRING = lxi.define("STRING", "\"(?:[^\\\"]|\\.)*\"|'(?:[^\\']|\\.)*'");
private static Token.Type ESC = lxi.define("ESC", "\\\\[^ux]|\\\\x[0-9a-fA-F][0-9a-fA-F]|\\\\u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]");
private static Token.Type SYM = lxi.define("SYM", "[.#*%@$^!+-]");
private static Token.Type WSPACE = lxi.define("WSPACE", "[\\s]+");
private static Token.Type EOF = lxi.eof("EOF");
private static Lexer LEXER = lxi.compile();
private static final int MAX_CACHED_ENTRIES = Integer.getInteger(ClientProperties.QPID_MAX_CACHED_ADDR_OPTION_STRINGS,
ClientProperties.DEFAULT_MAX_CACHED_ADDR_OPTION_STRINGS);
// stores address options maps for options strings that we have encountered; using a synchronizedMap wrapper
// in case multiple threads are parsing addresses.
private static Map<String, Map<Object, Object>> optionsMaps =
Collections.synchronizedMap(
new LinkedHashMap<String, Map<Object, Object>>(MAX_CACHED_ENTRIES +1,1.1f,true)
{
@Override
protected boolean removeEldestEntry(Map.Entry<String,Map<Object, Object>> eldest)
{
return size() > MAX_CACHED_ENTRIES;
}
});
public static List<Token> lex(String input)
{
return LEXER.lex(input);
}
static List<Token> wlex(String input)
{
List<Token> tokens = new ArrayList<Token>();
for (Token t : lex(input))
{
if (t.getType() != WSPACE)
{
tokens.add(t);
}
}
return tokens;
}
static String unquote(String st, Token tok)
{
StringBuilder result = new StringBuilder();
for (int i = 1; i < st.length() - 1; i++)
{
char ch = st.charAt(i);
if (ch == '\\')
{
char code = st.charAt(i+1);
switch (code)
{
case '\n':
break;
case '\\':
result.append('\\');
break;
case '\'':
result.append('\'');
break;
case '"':
result.append('"');
break;
case 'a':
result.append((char) 0x07);
break;
case 'b':
result.append((char) 0x08);
break;
case 'f':
result.append('\f');
break;
case 'n':
result.append('\n');
break;
case 'r':
result.append('\r');
break;
case 't':
result.append('\t');
break;
case 'u':
result.append(decode(st.substring(i+2, i+6)));
i += 4;
break;
case 'v':
result.append((char) 0x0b);
break;
case 'o':
result.append(decode(st.substring(i+2, i+4), 8));
i += 2;
break;
case 'x':
result.append(decode(st.substring(i+2, i+4)));
i += 2;
break;
default:
throw new ParseError(tok);
}
i += 1;
}
else
{
result.append(ch);
}
}
return result.toString();
}
static char[] decode(String hex)
{
return decode(hex, 16);
}
static char[] decode(String code, int radix)
{
return Character.toChars(Integer.parseInt(code, radix));
}
static String tok2str(Token tok)
{
Token.Type type = tok.getType();
String value = tok.getValue();
if (type == STRING)
{
return unquote(value, tok);
}
else if (type == ESC)
{
if (value.charAt(1) == 'x' || value.charAt(1) == 'u')
{
return new String(decode(value.substring(2)));
}
else
{
return value.substring(1);
}
}
else
{
return value;
}
}
static Object tok2obj(Token tok)
{
Token.Type type = tok.getType();
String value = tok.getValue();
if (type == STRING)
{
return unquote(value, tok);
}
else if (type == NUMBER)
{
if (value.indexOf('.') >= 0)
{
return Double.valueOf(value);
}
else
{
return Integer.decode(value);
}
}
else if (type == TRUE)
{
return true;
}
else if (type == FALSE)
{
return false;
}
else
{
return value;
}
}
static String toks2str(List<Token> toks)
{
if (toks.size() > 0)
{
StringBuilder result = new StringBuilder();
for (Token t : toks)
{
result.append(tok2str(t));
}
return result.toString();
}
else
{
return null;
}
}
public AddressParser(String input)
{
super(wlex(input));
}
public Address parse()
{
Address result = address();
eat(EOF);
return result;
}
public Address address()
{
String name = toks2str(eat_until(SLASH, SEMI, EOF));
if (name == null)
{
throw new ParseError(next());
}
String subject;
if (matches(SLASH))
{
eat(SLASH);
subject = toks2str(eat_until(SEMI, EOF));
if ("None".equals(subject))
{
subject = null;
}
}
else
{
subject = null;
}
Map<Object, Object> options;
if (matches(SEMI))
{
eat(SEMI);
// get the remaining string denoting the options and see if we've already encountered an address
// with the same options before
String optionsString = toks2str(remainder());
Map<Object,Object> storedMap = optionsMaps.get(optionsString);
if (storedMap == null)
{
// if these are new options, construct a new map and store it in the encountered collection
options = Collections.unmodifiableMap(map());
optionsMaps.put(optionsString, options);
}
else
{
// if we already have the map for these options, use the stored map
options = storedMap;
eat_until(EOF);
}
}
else
{
options = null;
}
return new Address(name, subject, options);
}
public Map<Object,Object> map()
{
eat(LBRACE);
Map<Object,Object> result = new HashMap<Object,Object>();
while (true)
{
if (matches(NUMBER, STRING, ID, LBRACE, LBRACK))
{
keyval(result);
if (matches(COMMA))
{
eat(COMMA);
}
else if (matches(RBRACE))
{
break;
}
else
{
throw new ParseError(next(), COMMA, RBRACE);
}
}
else if (matches(RBRACE))
{
break;
}
else
{
throw new ParseError(next(), NUMBER, STRING, ID, LBRACE, LBRACK,
RBRACE);
}
}
eat(RBRACE);
return result;
}
void keyval(Map<Object,Object> map)
{
Object key = value();
eat(COLON);
Object val = value();
map.put(key, val);
}
Object value()
{
if (matches(NUMBER, STRING, ID, TRUE, FALSE))
{
return tok2obj(eat());
}
else if (matches(LBRACE))
{
return map();
}
else if (matches(LBRACK))
{
return list();
}
else
{
throw new ParseError(next(), NUMBER, STRING, ID, LBRACE, LBRACK);
}
}
List<Object> list()
{
eat(LBRACK);
List<Object> result = new ArrayList<Object>();
while (true)
{
if (matches(RBRACK))
{
break;
}
else
{
result.add(value());
if (matches(COMMA))
{
eat(COMMA);
}
else if (matches(RBRACK))
{
break;
}
else
{
throw new ParseError(next(), COMMA, RBRACK);
}
}
}
eat(RBRACK);
return result;
}
}