//
// Copyright 2010 Cinch Logic Pty Ltd.
//
// http://www.chililog.com
//
// Licensed 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.chililog.server.data;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import org.bson.BSONCallback;
import com.mongodb.util.JSONCallback;
/**
* <p>
* Parser for JSON objects and converts them into mongoDB DBObjects.
* </p>
* <p>
* Supports all types described at www.json.org, except for numbers with "e" or "E" in them.
* </p>
* <p>
* Modified from https://github.com/mongodb/mongo-java-driver/blob/master/src/main/com/mongodb/util/JSON.java.
* </p>
* <p>
* Modified to support:
* <ul>
* <li>Long - Number >10 digits or string of digits with L at the end "123412341234L"</li>
* <li>Date - Format is as per specified on the constructor.</li>
* </p>
*/
public class MongoJsonParser {
private Pattern _datePattern = null;
private Pattern _longNumberPattern = null;
private SimpleDateFormat _dateFormat = null;
String s;
int pos = 0;
BSONCallback _callback;
/**
* Create a new parser without parsing strings for dates and time.
*
* @param s
* String to parse
*/
public MongoJsonParser(String s) {
this(s, null);
}
/**
* Create a new parser and parse string values for dates and times as per the specified formats.
*
* @param s
* String to parse
* @param datePattern
* Regular expression to use to test if a string is a date. Group #1 in the pattern is used. If null, no
* date checking is performed.
* @param dateFormat
* {@link SimpleDateFormat} pattern to use to parse the date. If null, no date parsing is performed.
* @param longNumberPattern
* Regular expression to use to test if a string is a long number. Group #1 in the pattern is used. If
* null, no long number matching is performed.
*/
public MongoJsonParser(String s, Pattern datePattern, String dateFormat, Pattern longNumberPattern) {
this(s, null);
_datePattern = datePattern;
_dateFormat = StringUtils.isBlank(dateFormat) ? null : new SimpleDateFormat(dateFormat);
_longNumberPattern = longNumberPattern;
}
/**
* Create a new parser.
*/
private MongoJsonParser(String s, BSONCallback callback) {
this.s = s;
_callback = (callback == null) ? new JSONCallback() : callback;
}
/**
* Parse an unknown type.
*
* @return Object the next item
* @throws JSONParseException
* if invalid JSON is found
*/
public Object parse() {
return parse(null);
}
/**
* Parse an unknown type.
*
* @return Object the next item
* @throws JSONParseException
* if invalid JSON is found
*/
protected Object parse(String name) {
Object value = null;
char current = get();
switch (current) {
// null
case 'n':
read('n');
read('u');
read('l');
read('l');
value = null;
break;
// true
case 't':
read('t');
read('r');
read('u');
read('e');
value = true;
break;
// false
case 'f':
read('f');
read('a');
read('l');
read('s');
read('e');
value = false;
break;
// string
case '\'':
case '\"':
String stringValue = parseString();
// Check for long
value = stringValue;
if (!StringUtils.isBlank(stringValue)) {
if (_longNumberPattern != null) {
Matcher m = _longNumberPattern.matcher(stringValue);
if (m.matches()) {
try {
value = Long.parseLong(m.group(1));
} catch (Exception ex) {
throw new JSONParseException(s, pos);
}
}
}
if (_dateFormat != null && _datePattern != null) {
Matcher m = _datePattern.matcher(stringValue);
if (m.matches()) {
try {
String dateString = m.group(1);
if (dateString.endsWith("Z")) {
// Simple date format does not recognise Z time zone so make it GMT
dateString = dateString.substring(0, dateString.length() - 1) + "GMT";
}
value = _dateFormat.parse(dateString);
} catch (Exception ex) {
throw new JSONParseException(ex, s, pos);
}
}
}
}
break;
// number
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case '+':
case '-':
value = parseNumber();
break;
// array
case '[':
value = parseArray(name);
break;
// object
case '{':
value = parseObject(name);
break;
default:
throw new JSONParseException(s, pos);
}
return value;
}
/**
* Parses an object for the form <i>{}</i> and <i>{ members }</i>.
*
* @return DBObject the next object
* @throws JSONParseException
* if invalid JSON is found
*/
public Object parseObject() {
return parseObject(null);
}
/**
* Parses an object for the form <i>{}</i> and <i>{ members }</i>.
*
* @return DBObject the next object
* @throws JSONParseException
* if invalid JSON is found
*/
protected Object parseObject(String name) {
if (name != null) {
_callback.objectStart(name);
} else {
_callback.objectStart();
}
read('{');
@SuppressWarnings("unused")
char current = get();
while (get() != '}') {
String key = parseString();
read(':');
Object value = parse(key);
doCallback(key, value);
if ((current = get()) == ',') {
read(',');
} else {
break;
}
}
read('}');
return _callback.objectDone();
}
protected void doCallback(String name, Object value) {
if (value == null) {
_callback.gotNull(name);
} else if (value instanceof String) {
_callback.gotString(name, (String) value);
} else if (value instanceof Boolean) {
_callback.gotBoolean(name, (Boolean) value);
} else if (value instanceof Integer) {
_callback.gotInt(name, (Integer) value);
} else if (value instanceof Long) {
_callback.gotLong(name, (Long) value);
} else if (value instanceof Double) {
_callback.gotDouble(name, (Double) value);
} else if (value instanceof Date) {
_callback.gotDate(name, ((Date) value).getTime());
}
}
/**
* Read the current character, making sure that it is the expected character. Advances the pointer to the next
* character.
*
* @param ch
* the character expected
*
* @throws JSONParseException
* if the current character does not match the given character
*/
public void read(char ch) {
if (!check(ch)) {
throw new JSONParseException(s, pos);
}
pos++;
}
public char read() {
if (pos >= s.length())
throw new IllegalStateException("string done");
return s.charAt(pos++);
}
/**
* Read the current character, making sure that it is a hexidecimal character.
*
* @throws JSONParseException
* if the current character is not a hexidecimal character
*/
public void readHex() {
if (pos < s.length()
&& ((s.charAt(pos) >= '0' && s.charAt(pos) <= '9') || (s.charAt(pos) >= 'A' && s.charAt(pos) <= 'F') || (s
.charAt(pos) >= 'a' && s.charAt(pos) <= 'f'))) {
pos++;
} else {
throw new JSONParseException(s, pos);
}
}
/**
* Checks the current character, making sure that it is the expected character.
*
* @param ch
* the character expected
*
* @throws JSONParseException
* if the current character does not match the given character
*/
public boolean check(char ch) {
return get() == ch;
}
/**
* Advances the position in the string past any whitespace.
*/
public void skipWS() {
while (pos < s.length() && Character.isWhitespace(s.charAt(pos))) {
pos++;
}
}
/**
* Returns the current character. Returns -1 if there are no more characters.
*
* @return the next character
*/
public char get() {
skipWS();
if (pos < s.length())
return s.charAt(pos);
return (char) -1;
}
/**
* Parses a string.
*
* @return the next string.
* @throws JSONParseException
* if invalid JSON is found
*/
public String parseString() {
char quot;
if (check('\''))
quot = '\'';
else if (check('\"'))
quot = '\"';
else
throw new JSONParseException(s, pos);
char current;
read(quot);
StringBuilder buf = new StringBuilder();
int start = pos;
while (pos < s.length() && (current = s.charAt(pos)) != quot) {
if (current == '\\') {
pos++;
char x = get();
char special = 0;
switch (x) {
case 'u': { // decode unicode
buf.append(s.substring(start, pos - 1));
pos++;
int tempPos = pos;
readHex();
readHex();
readHex();
readHex();
int codePoint = Integer.parseInt(s.substring(tempPos, tempPos + 4), 16);
buf.append((char) codePoint);
start = pos;
continue;
}
case 'n':
special = '\n';
break;
case 'r':
special = '\r';
break;
case 't':
special = '\t';
break;
case 'b':
special = '\b';
break;
case '"':
special = '\"';
break;
case '\\':
special = '\\';
break;
}
buf.append(s.substring(start, pos - 1));
if (special != 0) {
pos++;
buf.append(special);
}
start = pos;
continue;
}
pos++;
}
read(quot);
buf.append(s.substring(start, pos - 1));
return buf.toString();
}
/**
* Parses a number.
*
* @return the next number (int or double).
* @throws JSONParseException
* if invalid JSON is found
*/
public Number parseNumber() {
@SuppressWarnings("unused")
char current = get();
int start = this.pos;
boolean isDouble = false;
if (check('-') || check('+')) {
pos++;
}
outer: while (pos < s.length()) {
switch (s.charAt(pos)) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
pos++;
break;
case '.':
isDouble = true;
parseFraction();
break;
case 'e':
case 'E':
isDouble = true;
parseExponent();
break;
default:
break outer;
}
}
if (isDouble)
return Double.valueOf(s.substring(start, pos));
if (pos - start >= 10)
return Long.valueOf(s.substring(start, pos));
return Integer.valueOf(s.substring(start, pos));
}
/**
* Advances the pointed through <i>.digits</i>.
*/
public void parseFraction() {
// get past .
pos++;
outer: while (pos < s.length()) {
switch (s.charAt(pos)) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
pos++;
break;
case 'e':
case 'E':
parseExponent();
break;
default:
break outer;
}
}
}
/**
* Advances the pointer through the exponent.
*/
public void parseExponent() {
// get past E
pos++;
if (check('-') || check('+')) {
pos++;
}
outer: while (pos < s.length()) {
switch (s.charAt(pos)) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
pos++;
break;
default:
break outer;
}
}
}
/**
* Parses the next array.
*
* @return the array
* @throws JSONParseException
* if invalid JSON is found
*/
public Object parseArray() {
return parseArray(null);
}
/**
* Parses the next array.
*
* @return the array
* @throws JSONParseException
* if invalid JSON is found
*/
protected Object parseArray(String name) {
if (name != null) {
_callback.arrayStart(name);
} else {
_callback.arrayStart();
}
read('[');
int i = 0;
char current = get();
while (current != ']') {
String elemName = String.valueOf(i++);
Object elem = parse(elemName);
doCallback(elemName, elem);
if ((current = get()) == ',') {
read(',');
} else if (current == ']') {
break;
} else {
throw new JSONParseException(s, pos);
}
}
read(']');
return _callback.arrayDone();
}
/**
* Exception throw when invalid JSON is passed to JSONParser.
*
* This exception creates a message that points to the first offending character in the JSON string:
*
* <pre>
* { "x" : 3, "y" : 4, some invalid json.... }
* ^
* </pre>
*/
static class JSONParseException extends RuntimeException {
private static final long serialVersionUID = -4415279469780082174L;
String s;
int pos;
public String getMessage() {
StringBuilder sb = new StringBuilder();
sb.append("\n");
sb.append(s);
sb.append("\n");
for (int i = 0; i < pos; i++) {
sb.append(" ");
}
sb.append("^");
return sb.toString();
}
public JSONParseException(String s, int pos) {
this.s = s;
this.pos = pos;
}
public JSONParseException(Throwable ex, String s, int pos) {
super(ex);
this.s = s;
this.pos = pos;
}
}
}