/**
* Copyright (c) 2007-2014, JAGaToo Project Group all rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of the 'Xith3D Project Group' nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) A
* RISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE
*/
package org.jagatoo.util.json;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.nio.charset.Charset;
import org.jagatoo.util.errorhandling.ParsingException;
import org.jagatoo.util.io.UnicodeBOM;
/**
* Parses general JSON files.
*
* @author Marvin Froehlich
*/
public class JSONParser
{
/**
* State transitions:
*
* Initial: SEARCHING_VALUE
*
* SEARCHING_VALUE -> [ READING_VALUE, SEARCHING_NAME, SEARCHING_ARRAY_ELEMENT ] // regular value found, OBJECT_BEGIN found, ARRAY_BEGIN found
* READING_NAME -> [ SEARCHING_DELIMITER, SEARCHING_VALUE ] // name completed, NAME_VALUE_DELIMITER found without white space
* SEARCHING_DELIMITER -> [ SEARCHING_VALUE ]
* SEARCHING_VALUE -> [ READING_VALUE, SEARCHING_NAME, SEARCHING_ARRAY_ELEMENT ] // regular value found, OBJECT_BEGIN found, ARRAY_BEGIN found
* SEARCHING_ARRAY_ELEMENT -> [ READING_VALUE, SEARCHING_NAME, SEARCHING_ARRAY_ELEMENT ] // regular entry found, OBJECT_BEGIN found, ARRAY_BEGIN found
* READING_VALUE -> [ SEARCHING_TERMINATOR, SEARCHING_NAME, SEARCHING_ARRAY_ELEMENT ] // regular entry found, OBJECT_BEGIN found, terminator found
* SEARCHING_TERMINATOR
*
* @author Marvin Froehlich
*/
public static final char OBJECT_BEGIN = '{';
public static final char OBJECT_END = '}';
public static final char ARRAY_BEGIN = '[';
public static final char ARRAY_END = ']';
public static final char ESCAPE_CHAR = '\\';
public static final char QUOTE = '\"';
public static final char NEW_LINE = '\n';
public static final char NAME_VALUE_DELIMITER = ':';
public static final char ELEMENT_DELIMITER = ',';
/**
* Creates a new regular named object.
*
* @param meta the parse meta data
* @param name the object's name
*
* @return the new object.
*/
protected Object newObject( ParseMeta meta, String name )
{
return ( new java.util.HashMap<String, Object>() );
//return ( new SimpleMap() );
}
/**
* Creates a new regular named object.
*
* @param meta the parse meta data (stack includes the current object)
* @param objectName the current object's name
* @param object the current object
* @param childName the child object's name
* @param childValue the child object to add to object
*/
protected void addToObject( ParseMeta meta, String objectName, Object object, String childName, Object childValue )
{
@SuppressWarnings( "unchecked" )
java.util.Map<String, Object> map = (java.util.Map<String, Object>)object;
//SimpleMap map = (SimpleMap)object;
map.put( childName, childValue );
}
/**
* Creates a new array.
*
* @param meta the parse meta data
* @param name the array's name
* @param size the array's size
*
* @return the new object.
*/
protected Object[] newArray( ParseMeta meta, String name, int size )
{
return ( new Object[ size ] );
}
/**
*
* @param meta
* @param list
* @param arrayName
* @param value
*
* @return the index.
*/
protected int addToArray( ParseMeta meta, TempList list, String arrayName, Object value )
{
list.add( value, meta, arrayName );
return ( list.size() - 1 );
}
/**
* Creates a custom object from the raw value. The default implementation returns a Long, if couldBeNumeric is true and the value is parseable to a Long, or a Double otherwise
* and returns the raw value as a String, if couldBeNumeric is false.
*
* @param meta
* @param name
* @param rawValue
* @param couldBeNumeric
* @param couldBeBoolean
*
* @return the value object.
*/
protected Object newValue( ParseMeta meta, String name, String rawValue, boolean couldBeNumeric, boolean couldBeBoolean )
{
if ( couldBeNumeric )
{
try
{
long value = Long.parseLong( rawValue, 10 );
if ( ( value >= Integer.MIN_VALUE ) && ( value <= Integer.MAX_VALUE ) )
return ( (int)value );
return ( value );
}
catch ( NumberFormatException e )
{
return ( Double.valueOf( rawValue ) );
}
}
if ( couldBeBoolean )
return ( Boolean.parseBoolean( rawValue ) );
return ( rawValue );
}
/**
* Searches the begin of a name.
*
* @param br
* @param meta
*
* @return <code>true</code>, if a name was found, <code>false</code> otherwise (delimiter hit).
*
* @throws IOException
* @throws ParsingException
*/
protected boolean searchName( BufferedReader br, ParseMeta meta ) throws IOException, ParsingException
{
int ch_ = -1;
while ( ( ch_ = br.read() ) != -1 )
{
final char ch = (char)ch_;
if ( Character.isWhitespace( ch ) )
{
if ( ch == NEW_LINE )
meta.incLineNo();
}
else if ( ch == QUOTE )
{
meta.setLastChar( ch );
return ( true );
}
else if ( ( ( ch >= 'a' ) && ( ch <= 'z' ) ) || ( ( ch >= 'A' ) && ( ch <= 'Z' ) ) || ( ch == '_' ) )
{
meta.setLastChar( ch );
return ( true );
}
else if ( ch == ELEMENT_DELIMITER )
{
// skip
}
else if ( ch == OBJECT_END )
{
meta.setLastChar( ch );
return ( false );
}
else
{
throw new ParsingException( meta.getLineNo(), "Unexpected Character '" + ch + "' in line #" + meta.getLineNo() + "." );
}
}
throw new ParsingException( meta.getLineNo(), "Unexpected EOF at line #" + meta.getLineNo() + "." );
}
private static void rTrim( StringBuilder sb )
{
while ( ( sb.length() > 0 ) && Character.isWhitespace( sb.charAt( sb.length() - 1 ) ) )
sb.setLength( sb.length() - 1 );
}
/**
* Reads a name.
*
* @param br
* @param meta
*
* @return the name.
*
* @throws IOException
* @throws ParsingException
*/
protected String readName( BufferedReader br, ParseMeta meta ) throws IOException, ParsingException
{
StringBuilder sb = new StringBuilder();
if ( !searchName( br, meta ) )
return ( null );
boolean quoted = ( meta.getLastChar() == QUOTE );
if ( !quoted )
sb.append( meta.getLastChar() );
char prevChar = meta.getLastChar();
int ch_ = -1;
while ( ( ch_ = br.read() ) != -1 )
{
final char ch = (char)ch_;
if ( quoted && ( ch == QUOTE ) && ( prevChar != ESCAPE_CHAR ) )
{
meta.setLastChar( ch );
return ( sb.toString() );
}
else if ( !quoted && ( ch == NAME_VALUE_DELIMITER ) )
{
meta.setLastChar( ch );
rTrim( sb );
return ( sb.toString() );
}
else
{
if ( ch == NEW_LINE )
meta.incLineNo();
if ( ( ch == QUOTE ) && ( prevChar == ESCAPE_CHAR ) )
sb.setLength( sb.length() - 1 );
sb.append( ch );
}
prevChar = ch;
}
rTrim( sb );
return ( sb.toString() );
}
/**
* Searching the begin of a value.
*
* Transitions: SEARCHING_VALUE -> [ READING_VALUE, SEARCHING_NAME, SEARCHING_ARRAY_ELEMENT ] // regular value found, OBJECT_BEGIN found, ARRAY_BEGIN found
*
* @param br
* @param meta the parse meta data
*
* @return -1 for no value, 1 for quoted string value, 2 for true, 3 for false, 4 for numeric value, 5 for object, 6 for array
*
* @throws IOException
* @throws ParsingException
*/
protected int searchValue( BufferedReader br, ParseMeta meta ) throws IOException, ParsingException
{
int ch_ = -1;
while ( ( ch_ = br.read() ) != -1 )
{
final char ch = (char)ch_;
if ( Character.isWhitespace( ch ) )
{
if ( ch == NEW_LINE )
meta.incLineNo();
}
else if ( ch == OBJECT_BEGIN )
{
meta.setLastChar( ch );
return ( 5 );
}
else if ( !meta.isRoot() && ( ch == ELEMENT_DELIMITER ) )
{
// skip
}
else if ( !meta.isRoot() && ( ch == OBJECT_END ) )
{
return ( -1 );
}
else if ( !meta.isRoot() && ( ch == ARRAY_BEGIN ) )
{
meta.setLastChar( ch );
return ( 6 );
}
else if ( !meta.isRoot() && ( ch == ARRAY_END ) )
{
return ( -1 );
}
else if ( !meta.isRoot() && ( ch == QUOTE ) )
{
meta.setLastChar( ch );
return ( 1 );
}
else if ( !meta.isRoot() && ( ( ch == 't' ) || ( ch == 'T' ) ) )
{
meta.setLastChar( ch );
return ( 2 );
}
else if ( !meta.isRoot() && ( ( ch == 'f' ) || ( ch == 'F' ) ) )
{
meta.setLastChar( ch );
return ( 3 );
}
else if ( !meta.isRoot() && ( Character.isDigit( ch ) || ( ch == '.' ) || ( ch == '-' ) || ( ch == '+' ) ) )
{
meta.setLastChar( ch );
return ( 4 );
}
else
{
throw new ParsingException( meta.getLineNo(), "Unexpected Character '" + ch + "' in line #" + meta.getLineNo() + "." );
}
}
return ( -1 );
}
/**
* Searching the begin of a value.
*
* Transitions: SEARCHING_VALUE -> [ READING_VALUE, SEARCHING_NAME, SEARCHING_ARRAY_ELEMENT ] // regular value found, OBJECT_BEGIN found, ARRAY_BEGIN found
*
* @param br
* @param meta the parse meta data
*
* @throws IOException
* @throws ParsingException
*/
protected void searchNameValueDelimiter( BufferedReader br, ParseMeta meta ) throws IOException, ParsingException
{
int ch_ = -1;
while ( ( ch_ = br.read() ) != -1 )
{
final char ch = (char)ch_;
if ( Character.isWhitespace( ch ) )
{
if ( ch == NEW_LINE )
meta.incLineNo();
}
else if ( ch == NAME_VALUE_DELIMITER )
{
meta.setLastChar( ch );
return;
}
else
{
throw new ParsingException( meta.getLineNo(), "Unexpected Character '" + ch + "' in line #" + meta.getLineNo() + "." );
}
}
}
/**
* Reads a value.
*
* @param br
* @param meta
* @param parentObject
* @param name
*
* @return the value.
*
* @throws IOException
* @throws ParsingException
*/
protected Object readValue( BufferedReader br, ParseMeta meta, Object parentObject, String name ) throws IOException, ParsingException
{
StringBuilder sb = new StringBuilder();
int valueType = searchValue( br, meta );
if ( valueType < 0 )
return ( null );
long lineNo = meta.getLineNo();
meta.notRoot();
switch ( valueType )
{
case 1: // quoted string
case 2: // true
case 3: // false
case 4: // numeric value
boolean quoted = ( valueType == 1 );
if ( !quoted )
sb.append( meta.getLastChar() );
char prevChar = meta.getLastChar();
int ch_ = -1;
while ( ( ch_ = br.read() ) != -1 )
{
final char ch = (char)ch_;
if ( quoted && ( ch == QUOTE ) && ( prevChar != ESCAPE_CHAR ) )
{
meta.setLastChar( ch );
return ( newValue( meta, name, sb.toString(), false, false ) );
}
else if ( !quoted && ( ( ch == ELEMENT_DELIMITER ) || ( ch == OBJECT_END ) || ( ch == ARRAY_END ) ) )
{
meta.setLastChar( ch );
rTrim( sb );
if ( valueType == 2 )
{
String value = sb.toString();
if ( !value.equalsIgnoreCase( "true" ) )
throw new ParsingException( lineNo, "Unexpected value \"" + value + "\" in line #" + lineNo + "." );
return ( newValue( meta, name, sb.toString(), false, true ) );
}
if ( valueType == 3 )
{
String value = sb.toString();
if ( !value.equalsIgnoreCase( "false" ) )
throw new ParsingException( lineNo, "Unexpected value \"" + value + "\" in line #" + lineNo + "." );
return ( newValue( meta, name, sb.toString(), false, true ) );
}
return ( newValue( meta, name, sb.toString(), true, false ) );
}
else
{
if ( ch == NEW_LINE )
meta.incLineNo();
if ( ( ch == QUOTE ) && ( prevChar == ESCAPE_CHAR ) )
sb.setLength( sb.length() - 1 );
else if ( ( ch == '/' ) && ( prevChar == ESCAPE_CHAR ) )
sb.setLength( sb.length() - 1 );
else if ( ( ch == '\\' ) && ( prevChar == ESCAPE_CHAR ) )
sb.setLength( sb.length() - 1 );
sb.append( ch );
}
prevChar = ch;
}
rTrim( sb );
return ( sb.toString() );
case 5: // object
Object object = newObject( meta, name );
meta.pushToStack( name, object );
try
{
String childName = null;
while ( ( childName = readName( br, meta ) ) != null )
{
if ( meta.getLastChar() != NAME_VALUE_DELIMITER )
searchNameValueDelimiter( br, meta );
Object value = readValue( br, meta, object, childName );
addToObject( meta, name, object, childName, value );
if ( meta.consumeLastChar() == OBJECT_END )
break;
}
}
finally
{
meta.consumeLastChar();
meta.popFromStack();
}
return ( object );
case 6: // array
TempList list = new TempList( this, meta, name );
meta.pushToStack( name, list );
try
{
Object value = null;
int index = 0;
while ( ( value = readValue( br, meta, list, String.valueOf( index ) ) ) != null )
{
addToArray( meta, list, name, value );
index++;
char lc = meta.consumeLastChar();
if ( lc == OBJECT_END )
throw new ParsingException( meta.getLineNo(), "Unexpected Character '" + meta.getLastChar() + "' in line #" + meta.getLineNo() + "." );
if ( lc == ARRAY_END )
break;
}
}
finally
{
meta.consumeLastChar();
meta.popFromStack();
}
return ( list.getArray( meta, name ) );
}
throw new Error( "Unsupported value type: " + valueType );
}
protected void checkRemainingFileContents( BufferedReader br, ParseMeta meta ) throws IOException, ParsingException
{
int ch_ = -1;
while ( ( ch_ = br.read() ) != -1 )
{
final char ch = (char)ch_;
if ( Character.isWhitespace( ch ) )
{
if ( ch == NEW_LINE )
meta.incLineNo();
}
else
{
throw new ParsingException( meta.getLineNo(), "Unexpected Character '" + ch + "' in line #" + meta.getLineNo() + "." );
}
}
}
protected ParseMeta newParseMeta()
{
return ( new ParseMeta() );
}
protected Object parse( BufferedReader br ) throws IOException, ParsingException
{
try
{
ParseMeta meta = newParseMeta();
Object result = readValue( br, meta, null, null );
checkRemainingFileContents( br, meta );
return ( result );
}
finally
{
try
{
br.close();
}
catch ( IOException e )
{
}
}
}
public final Object parse( Reader r ) throws IOException, ParsingException
{
return ( parse( ( r instanceof BufferedReader ) ? (BufferedReader)r : new BufferedReader( r ) ) );
}
public final Object parse( InputStream in, Charset charset ) throws IOException, ParsingException
{
return ( parse( new InputStreamReader( in, charset ) ) );
}
public final Object parse( InputStream in, String charset ) throws IOException, ParsingException
{
return ( parse( new InputStreamReader( in, Charset.forName( charset ) ) ) );
}
public final Object parse( InputStream in ) throws IOException, ParsingException
{
BufferedInputStream bin = ( in instanceof BufferedInputStream ) ? (BufferedInputStream)in : new BufferedInputStream( in );
UnicodeBOM bom = UnicodeBOM.skipBOM( bin );
Charset charset = ( bom == null ) ? null : bom.getCharset();
if ( charset == null )
return ( parse( new InputStreamReader( in ) ) );
return ( parse( new InputStreamReader( in, charset ) ) );
}
public final Object parse( URL url, Charset charset ) throws IOException, ParsingException
{
return ( parse( new InputStreamReader( url.openStream(), charset ) ) );
}
public final Object parse( URL url, String charset ) throws IOException, ParsingException
{
return ( parse( new InputStreamReader( url.openStream(), Charset.forName( charset ) ) ) );
}
public final Object parse( URL url ) throws IOException, ParsingException
{
return ( parse( url.openStream() ) );
}
public final Object parse( File file, Charset charset ) throws IOException, ParsingException
{
return ( parse( new InputStreamReader( new FileInputStream( file ), charset ) ) );
}
public final Object parse( File file, String charset ) throws IOException, ParsingException
{
return ( parse( new InputStreamReader( new FileInputStream( file ), Charset.forName( charset ) ) ) );
}
public final Object parse( File file ) throws IOException, ParsingException
{
return ( parse( new FileInputStream( file ) ) );
}
public final Object parse( String filename, Charset charset ) throws IOException, ParsingException
{
return ( parse( new InputStreamReader( new FileInputStream( filename ), charset ) ) );
}
public final Object parse( String filename, String charset ) throws IOException, ParsingException
{
return ( parse( new InputStreamReader( new FileInputStream( filename ), Charset.forName( charset ) ) ) );
}
public final Object parse( String filename ) throws IOException, ParsingException
{
return ( parse(new FileInputStream( filename ) ) );
}
public JSONParser()
{
}
/*
public static void main( String[] args ) throws Throwable
{
Object object = new JSONParser().parse( "c:\\Spiele\\rFactor2\\UserData\\player\\Controller.JSON" );
System.out.println( object );
}
*/
}