/* * * 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.restapi; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.qpid.messaging.Address; import org.apache.qpid.messaging.util.Lexer; import org.apache.qpid.messaging.util.Lexicon; import org.apache.qpid.messaging.util.ParseError; import org.apache.qpid.messaging.util.Token; /** * AddressParser * * This JSONMapParser class used is mostly just a direct copy of org.apache.qpid.messaging.util.AddressParser * as it provides a handy mechanism to parse a JSON String into a Map which is the only JSON requirement that * we really need for QMF. * * Unfortunately there's a restriction/bug on the core AddressParser whereby it serialises integers into Java Integer * which means that long integer values aren't correctly stored. It's this restriction that gives Java Address * Strings a defacto 2GB queue size. I should really provide a patch for the *real* AddressParser but it's better * to add features covering "shorthand" forms for large values (e.g. k/K, m/M, g/G for kilo, mega, giga etc.) * to both the Java and C++ AddressParser to ensure maximum consistency. * * This AddressParser clone largely uses the classes from org.apache.qpid.messaging.util like the real AddressParser * but unfortunately the Parser class was package scope rather than public, so I've done some "copy and paste reuse" * to add the Parser methods into this version of AddressParser. I've also removed the bits that actually create * an Address as all we need to do is to parse into a java.util.Map. */ public class JSONMapParser { 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]+"); // Make test for true and false case insensitive. N.B. org.apache.qpid.messaging.util.AddressParser test is // case sensitive - not sure if that's a bug/oversight in the AddressParser?? private static Token.Type TRUE = lxi.define("TRUE", "(?i)True"); private static Token.Type FALSE = lxi.define("FALSE", "(?i)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(); /********** Copied from org.apache.qpid.messaging.util.Parser as Parser was package scope **********/ private List tokens; private int idx = 0; Token next() { return tokens.get(idx); } boolean matches(Token.Type ... types) { for (Token.Type t : types) { if (next().getType() == t) { return true; } } return false; } Token eat(Token.Type ... types) { if (types.length > 0 && !matches(types)) { throw new ParseError(next(), types); } else { Token t = next(); idx += 1; return t; } } /***************************************************************************************************/ public static List lex(String input) { return LEXER.lex(input); } static List wlex(String input) { List tokens = new ArrayList(); 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)); } /** * This method is the the main place where this class differs from org.apache.qpid.messaging.util.AddressParser. * If the token type is a STRING it checks for a number (with optional floating point) ending in K, M or G * and if it is of this type it creates a Long out of the float value multiplied by 1000, 1000000 or 1000000000. * If the token type is a NUMBER it tries to parse into an Integer like AddressParser, but if that fails it * tries to parse onto a Long which allows much larger integer values to be used. */ static Object tok2obj(Token tok) { Token.Type type = tok.getType(); String value = tok.getValue(); if (type == STRING) { value = unquote(value, tok); // Initial regex to check for a number (with optional floating point) ending in K, M or G if (value.matches("([0-9]*\\.[0-9]+|[0-9]+)\\s*[kKmMgG]")) { // If it's a numeric string perform the relevant multiplication and return as a Long. int length = value.length(); if (length > 1) { String end = value.substring(length - 1, length).toUpperCase(); String start = value.substring(0, length - 1).trim(); if (end.equals("K")) { return Long.valueOf((long)(Float.parseFloat(start) * 1000.0)); } else if (end.equals("M")) { return Long.valueOf((long)(Float.parseFloat(start) * 1000000.0)); } else if (end.equals("G")) { return Long.valueOf((long)(Float.parseFloat(start) * 1000000000.0)); } } return value; } else { return value; } } else if (type == NUMBER) { // This block extends the original AddressParser handling of NUMBER. It first attempts to parse the String // into an Integer in order to be backwards compatible with AddressParser however if this causes a // NumberFormatException it then attempts to parse into a Long. if (value.indexOf('.') >= 0) { return Double.valueOf(value); } else { try { return Integer.decode(value); } catch (NumberFormatException nfe) { return Long.decode(value); } } } else if (type == TRUE) { return true; } else if (type == FALSE) { return false; } else { return value; } } public JSONMapParser(String input) { this.tokens = wlex(input); // Copied from org.apache.qpid.messaging.util.Parser this.idx = 0; // Copied from org.apache.qpid.messaging.util.Parser } public Map map() { eat(LBRACE); Map result = new HashMap(); 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 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 list() { eat(LBRACK); List result = new ArrayList(); 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; } }