com.hfg.javascript.JsObjMap Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of com_hfg Show documentation
Show all versions of com_hfg Show documentation
com.hfg xml, html, svg, and bioinformatics utility library
package com.hfg.javascript;
import java.io.*;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.hfg.util.collection.OrderedMap;
import com.hfg.util.StringUtil;
import com.hfg.xml.XMLException;
//------------------------------------------------------------------------------
/**
A Javascript object (Map) container useful for creating JSON.
JSON RFC: http://www.ietf.org/rfc/rfc4627.txt
@author J. Alex Taylor, hairyfatguy.com
*/
//------------------------------------------------------------------------------
// com.hfg Library
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
// [email protected]
//------------------------------------------------------------------------------
public class JsObjMap extends OrderedMap implements JsCollection
{
private Set mUnquotedValueKeys;
private static final Pattern KEY_PATTERN = Pattern.compile("(.+?)\\s*:");
private static final Pattern UNQUOTED_VALUE_PATTERN = Pattern.compile("(.+?)\\s*[,}]");
//--------------------------------------------------------------------------
public JsObjMap()
{
}
//--------------------------------------------------------------------------
public JsObjMap(int inInitialCapacity)
{
super(inInitialCapacity);
}
//--------------------------------------------------------------------------
public JsObjMap(int inInitialCapacity, float inLoadFactor)
{
super(inInitialCapacity, inLoadFactor);
}
//--------------------------------------------------------------------------
public JsObjMap(String inJSONString)
{
parse(inJSONString);
}
//###########################################################################
// PUBLIC METHODS
//###########################################################################
//--------------------------------------------------------------------------
@Override
public String toString()
{
return toJavascript();
}
//--------------------------------------------------------------------------
public String toJSON()
{
ByteArrayOutputStream outStream;
try
{
outStream = new ByteArrayOutputStream(2048);
toJSON(outStream);
outStream.close();
}
catch (Exception e)
{
throw new XMLException(e);
}
return outStream.toString();
}
//---------------------------------------------------------------------------
public void toJSON(OutputStream inStream)
{
PrintWriter writer = new PrintWriter(inStream);
toJSON(writer);
writer.flush();
}
//--------------------------------------------------------------------------
public void toJSON(Writer inWriter)
{
try
{
inWriter.write("{ ");
int i = 0;
for (String key : keySet())
{
if (i > 0)
{
inWriter.write(", ");
}
inWriter.write("\"");
inWriter.write(JSONUtil.escapeString(key));
inWriter.write("\": ");
inWriter.write(composeValueJSON(get(key)));
i++;
}
inWriter.write(" }");
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
//--------------------------------------------------------------------------
/**
Produces a javascript serialization that will generally be the same as produced
by toJSON() except when values have been added via the putUnquoted() method.
*/
public String toJavascript()
{
ByteArrayOutputStream outStream;
try
{
outStream = new ByteArrayOutputStream(2048);
toJavascript(outStream);
outStream.close();
}
catch (Exception e)
{
throw new XMLException(e);
}
return outStream.toString();
}
//---------------------------------------------------------------------------
/**
Produces a javascript serialization that will generally be the same as produced
by toJSON() except when values have been added via the putUnquoted() method.
*/
public void toJavascript(OutputStream inStream)
{
PrintWriter writer = new PrintWriter(inStream);
toJavascript(writer);
writer.flush();
}
//--------------------------------------------------------------------------
/**
Produces a javascript serialization that will generally be the same as produced
by toJSON() except when values have been added via the putUnquoted() method.
*/
public void toJavascript(Writer inWriter)
{
try
{
inWriter.write("{ ");
int i = 0;
for (String key : keySet())
{
if (i > 0)
{
inWriter.write(", ");
}
inWriter.write("\"");
inWriter.write(JSONUtil.escapeString(key));
inWriter.write("\": ");
if (mUnquotedValueKeys != null
&& mUnquotedValueKeys.contains(key))
{
inWriter.write(get(key).toString());
}
else
{
inWriter.write(composeValueJavascript(get(key)));
}
i++;
}
inWriter.write(" }");
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
//--------------------------------------------------------------------------
public String getString(String inKey)
{
Object value = get(inKey);
return (value != null ? value.toString() : null);
}
//--------------------------------------------------------------------------
/**
Puts the specified key / value pair into the map but does not quote the
value when calling toJavascript() [Note that not quoting a string value means
that the resulting serialization will no longer be valid JSON, but sending
function definitions in a JSON-like format can be useful when working with
javascript frameworks]. Numeric and boolean values should not be passed to
this method as they will be handle correctly by the standard put() method.
*/
public void putUnquoted(String inKey, Object inValue)
{
put(inKey, inValue);
if (null == mUnquotedValueKeys)
{
mUnquotedValueKeys = new HashSet<>(5);
}
mUnquotedValueKeys.add(inKey);
}
//--------------------------------------------------------------------------
public Object put(String inKey, JsArray inValue)
{
if (null == inKey)
{
throw new RuntimeException("A non-null value must be specified as the key!");
}
return super.put(inKey, inValue);
}
//--------------------------------------------------------------------------
public Object put(String inKey, Collection inValues)
{
if (null == inKey)
{
throw new RuntimeException("A non-null value must be specified as the key!");
}
return super.put(inKey, inValues != null ? new JsArray(inValues) : null);
}
//--------------------------------------------------------------------------
public Object put(String inKey, Object[] inValues)
{
if (null == inKey)
{
throw new RuntimeException("A non-null value must be specified as the key!");
}
return super.put(inKey, inValues != null ? new JsArray(inValues) : null);
}
//--------------------------------------------------------------------------
@Override
public Object remove(Object inKey)
{
if (mUnquotedValueKeys != null)
{
mUnquotedValueKeys.remove(inKey);
}
return super.remove(inKey);
}
//###########################################################################
// PRIVATE METHODS
//###########################################################################
//--------------------------------------------------------------------------
private String composeValueJSON(Object inValue)
{
String value = "null";
if (inValue != null)
{
if (inValue instanceof Number
|| inValue instanceof Boolean)
{
// Numeric and boolean values are written without quotes
value = inValue.toString();
}
else if (inValue instanceof JsArray)
{
value = ((JsArray) inValue).toJSON();
}
else if (inValue instanceof JsObjMap)
{
value = ((JsObjMap) inValue).toJSON();
}
else
{
// String values should be enclosed in double quotes
value = "\"" + JSONUtil.escapeString(inValue.toString()) + "\"";
}
}
return value;
}
//--------------------------------------------------------------------------
private String composeValueJavascript(Object inValue)
{
String value = "null";
if (inValue != null)
{
if (inValue instanceof Number
|| inValue instanceof Boolean)
{
value = inValue.toString();
}
else if (inValue instanceof JsArray)
{
value = ((JsArray) inValue).toJavascript();
}
else if (inValue instanceof JsObjMap)
{
value = ((JsObjMap) inValue).toJavascript();
}
else
{
value = "\"" + JSONUtil.escapeString(inValue.toString()) + "\"";
}
}
return value;
}
//--------------------------------------------------------------------------
private void parse(String inString)
{
String str = inString.trim();
if (str.charAt(0) != '{'
|| str.charAt(str.length() - 1) != '}')
{
throw new RuntimeException("JSON map must be enclosed with {}!");
}
int index = 1;
String key = null;
while (index < str.length() - 1)
{
char theChar = str.charAt(index);
if (Character.isWhitespace(theChar))
{
index++;
}
else if (theChar == ',')
{
index++;
}
else if (theChar == '[')
{
// Find the ending bracket
if (null == key)
{
throw new RuntimeException("Poorly structured JSON! Array at index " + index + " in map does not have a key!");
}
int depth = 1;
char quote = ' ';
boolean inEscape = false;
boolean inValue = false;
int end = index + 1;
while (end < str.length() -1
&& (depth != 1
|| inValue
|| str.charAt(end) != ']'))
{
char theEndChar = str.charAt(end);
if (theEndChar == '\\'
&& inValue)
{
inEscape = ! inEscape;
}
else if (inEscape)
{
inEscape = false;
}
if (inEscape)
{
continue;
}
if (theEndChar == '\''
&& (str.charAt(end -1) != '\\'
|| ! inEscape))
{
if (! inValue)
{
inValue = true;
quote = '\'';
}
else if (quote == '\'')
{
inValue = false;
}
}
else if (theEndChar == '"'
&& (str.charAt(end -1) != '\\'
|| ! inEscape))
{
if (! inValue)
{
inValue = true;
quote = '"';
}
else if (quote == '"')
{
inValue = false;
}
}
else if (! inValue
&& theEndChar == '[')
{
depth++;
}
else if (! inValue
&& theEndChar == ']')
{
depth--;
}
end++;
}
JsArray value = new JsArray(str.substring(index, end + 1));
put(key, value);
key = null;
index = end + 1;
}
else if (theChar == '{')
{
if (null == key)
{
throw new RuntimeException("Poorly structured JSON! Submap at index " + index + " in map does not have a key!");
}
// Find the ending brace
int depth = 1;
char quote = ' ';
boolean inEscape = false;
boolean inValue = false;
int end = index + 1;
while (end < str.length() -1
&& (depth != 1
|| inValue
|| str.charAt(end) != '}'))
{
char theEndChar = str.charAt(end);
if (theEndChar == '\\'
&& inValue)
{
inEscape = ! inEscape;
}
else if (inEscape)
{
inEscape = false;
}
if (inEscape)
{
continue;
}
if (theEndChar == '\''
&& (str.charAt(end -1) != '\\'
|| ! inEscape))
{
if (! inValue)
{
inValue = true;
quote = '\'';
}
else if (quote == '\'')
{
inValue = false;
}
}
else if (theEndChar == '"'
&& (str.charAt(end -1) != '\\'
|| ! inEscape))
{
if (! inValue)
{
inValue = true;
quote = '"';
}
else if (quote == '"')
{
inValue = false;
}
}
else if (! inValue
&& theEndChar == '{')
{
depth++;
}
else if (! inValue
&& theEndChar == '}')
{
depth--;
}
end++;
}
JsObjMap value = new JsObjMap(str.substring(index, end + 1));
put(key, value);
key = null;
index = end + 1;
}
else if (key != null)
{
if (theChar == '"')
{
boolean inEscape = false;
int valueStartIndex = index;
StringBuilder valueBuffer = new StringBuilder();
// Continue until we find the next unescaped quote.
while ((index++) < str.length() - 1
&& ((theChar = str.charAt(index)) != '"') || inEscape)
{
if (theChar == '\\')
{
inEscape = !inEscape;
}
else if (inEscape)
{
inEscape = false;
}
// if (! inEscape)
{
valueBuffer.append(theChar);
}
}
if (index == str.length() - 1)
{
throw new RuntimeException("Problem parsing value @ position " + valueStartIndex + " in JSON string!");
}
index++; // consume the trailing quote
put(key, JSONUtil.unescapeString(valueBuffer.toString()));
// put(key, valueBuffer.toString());
}
else
{
Matcher m = UNQUOTED_VALUE_PATTERN.matcher(str);
if (m.find(index))
{
String stringValue = JSONUtil.unescapeString(m.group(1));
index = m.end() - 1;
put(key, JSONUtil.convertStringValueToObject(stringValue));
}
}
key = null;
}
else
{
// Parse the key name
// Is it in quotes?
if (theChar == '"')
{
boolean inEscape = false;
int valueStartIndex = index;
StringBuilder valueBuffer = new StringBuilder();
// Continue until we find the next unescaped quote.
while ((index++) < str.length() - 1
&& ((theChar = str.charAt(index)) != '"') || inEscape)
{
if (theChar == '\\')
{
inEscape = !inEscape;
}
else if (inEscape)
{
inEscape = false;
}
// if (! inEscape)
{
valueBuffer.append(theChar);
}
}
if (index == str.length() - 1)
{
throw new RuntimeException("Problem parsing value @ position " + valueStartIndex + " in JSON string!");
}
index++; // consume the trailing quote
key = JSONUtil.unescapeString(valueBuffer.toString());
while (index < str.length() - 1
&& str.charAt(index++) != ':')
{
}
if (str.charAt(index - 1) != ':')
{
throw new RuntimeException("Problem with format of hash string: " + StringUtil.singleQuote(inString) + "!");
}
}
else
{
Matcher m = KEY_PATTERN.matcher(str);
if (m.find(index))
{
key = JSONUtil.unescapeString(StringUtil.unquote(m.group(1)));
// key = StringUtil.unquote(m.group(1));
index = m.end();
}
else
{
throw new RuntimeException("Problem with format of hash string: " + StringUtil.singleQuote(inString) + "!");
}
}
}
}
}
}