From fb8c345afa692b8c00569dbc4bf299aa46753993 Mon Sep 17 00:00:00 2001 From: Chris Walquist Date: Tue, 29 Jul 2014 11:25:07 -0500 Subject: [PATCH] Added finishTime, triggeredBy, statusText, to support existing brokenbuilds report. Added some JSON parsing to handle fields that could have metacharacters (couldn't find a jar'd version of the source out there, so just pulled it in for now) --- .../cedarsoftware/util/io/JsonObject.java | 360 ++ .../cedarsoftware/util/io/JsonReader.java | 3353 +++++++++++++++++ .../cedarsoftware/util/io/JsonWriter.java | 2061 ++++++++++ .../cread/teamcity/JSONMonitorController.java | 15 +- src/main/io/cread/teamcity/JSONViewState.java | 33 +- src/main/io/cread/teamcity/JobState.java | 11 +- .../io/cread/teamcity/JSONViewStateTests.java | 54 +- 7 files changed, 5861 insertions(+), 26 deletions(-) create mode 100644 src/main/cedarsoftware/util/io/JsonObject.java create mode 100644 src/main/cedarsoftware/util/io/JsonReader.java create mode 100644 src/main/cedarsoftware/util/io/JsonWriter.java diff --git a/src/main/cedarsoftware/util/io/JsonObject.java b/src/main/cedarsoftware/util/io/JsonObject.java new file mode 100644 index 0000000..ffafba4 --- /dev/null +++ b/src/main/cedarsoftware/util/io/JsonObject.java @@ -0,0 +1,360 @@ +package cedarsoftware.util.io; +// Downloaded 28 July 2014 from master branch of https://github.com/jdereg/json-io. +// Couldn't find a build of the jar, and didn't want to mess with Maven + +import java.io.IOException; +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * This class holds a JSON object in a LinkedHashMap. + * LinkedHashMap used to keep fields in same order as they are + * when reflecting them in Java. Instances of this class hold a + * Map-of-Map representation of a Java object, read from the JSON + * input stream. + * + * @param field name in Map-of-Map + * @param Value + * + * @author John DeRegnaucourt (jdereg@gmail.com) + *
+ * Copyright (c) Cedar Software LLC + *

+ * 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.* + */ +public class JsonObject extends LinkedHashMap +{ + Object target; + boolean isMap = false; + String type; + long id = -1; + int line; + int col; + + public long getId() + { + return id; + } + + public boolean hasId() + { + return id != -1; + } + + public void setType(String type) + { + this.type = type != null ? type.intern() : null; + } + + public String getType() + { + return type; + } + + public Object getTarget() + { + return target; + } + + public void setTarget(Object target) + { + this.target = target; + } + + public Class getTargetClass() + { + return target.getClass(); + } + + public boolean isPrimitive() + { + return ("int".equals(type) || "long".equals(type) || "boolean".equals(type) || "double".equals(type) || + "byte".equals(type) || "short".equals(type) || "float".equals(type) || "char".equals(type)); + } + + public static boolean isPrimitiveWrapper(Class c) + { + return Integer.class.equals(c) || Long.class.equals(c) || Boolean.class.equals(c) || Double.class.equals(c) || + Byte.class.equals(c) || Short.class.equals(c) || Float.class.equals(c) || Character.class.equals(c); + } + + public Object getPrimitiveValue() throws IOException + { + if ("int".equals(type)) + { + Number integer = (Number) get("value"); + return integer.intValue(); + } + if ("long".equals(type) || "boolean".equals(type) || "double".equals(type)) + { + return get("value"); + } + if ("byte".equals(type)) + { + Number b = (Number) get("value"); + return b.byteValue(); + } + if ("float".equals(type)) + { + Number f = (Number) get("value"); + return f.floatValue(); + } + if ("short".equals(type)) + { + Number s = (Number) get("value"); + return s.shortValue(); + } + if ("char".equals(type)) + { + String s = (String) get("value"); + return s.charAt(0); + } + return JsonReader.error("Invalid primitive type"); + } + + // Map APIs + public boolean isMap() + { + if (isMap || target instanceof Map) + { + return true; + } + + if (type == null) + { + return false; + } + try + { + Class c = JsonReader.classForName2(type); + if (Map.class.isAssignableFrom(c)) + { + return true; + } + } + catch (IOException ignored) { } + + return false; + + } + + // Collection APIs + public boolean isCollection() + { + if (containsKey("@items") && !containsKey("@keys")) + { + return ((target instanceof Collection) || (type != null && !type.contains("["))); + } + + if (type == null) + { + return false; + } + try + { + Class c = JsonReader.classForName2(type); + if (Collection.class.isAssignableFrom(c)) + { + return true; + } + } + catch (IOException ignored) { } + + return false; + } + + // Array APIs + public boolean isArray() + { + if (target == null) + { + if (type != null) + { + return type.contains("["); + } + return containsKey("@items") && !containsKey("@keys"); + } + return target.getClass().isArray(); + } + + public Object[] getArray() + { + return (Object[]) get("@items"); + } + + public int getLength() throws IOException + { + if (isArray()) + { + if (target == null) + { + Object[] items = (Object[]) get("@items"); + return items.length; + } + return Array.getLength(target); + } + if (isCollection() || isMap()) + { + Object[] items = (Object[]) get("@items"); + return items == null ? 0 : items.length; + } + throw new IllegalStateException("getLength() called on a non-collection, line " + line + ", col " + col); + } + + public Class getComponentType() + { + return target.getClass().getComponentType(); + } + + void moveBytesToMate() + { + byte[] bytes = (byte[]) target; + Object[] items = getArray(); + int len = items.length; + + for (int i = 0; i < len; i++) + { + bytes[i] = ((Number) items[i]).byteValue(); + } + } + + void moveCharsToMate() + { + Object[] items = getArray(); + if (items == null) + { + target = null; + } + else if (items.length == 0) + { + target = new char[0]; + } + else if (items.length == 1) + { + String s = (String) items[0]; + target = s.toCharArray(); + } + else + { + throw new IllegalStateException("char[] should only have one String in the [], found " + items.length + ", line " + line + ", col " + col); + } + } + + public V put(K key, V value) + { + if (key == null) + { + return super.put(key, value); + } + + if (key.equals("@type")) + { + String oldType = type; + type = (String) value; + return (V) oldType; + } + else if (key.equals("@id")) + { + Long oldId = id; + id = (Long) value; + return (V) oldId; + } + else if (("@items".equals(key) && containsKey("@keys")) || ("@keys".equals(key) && containsKey("@items"))) + { + isMap = true; + } + return super.put(key, value); + } + + public void clear() + { + super.clear(); + type = null; + } + + void clearArray() + { + remove("@items"); + } + + /** + * This method is deprecated. Use getLine() and getCol() to determine where this object was read + * from in the JSON stream. + * @return int line number where this object was read from + */ + @Deprecated + public long getPos() + { + return line; + } + + /** + * @return int line where this object '{' started in the JSON stream + */ + public int getLine() + { + return line; + } + + /** + * @return int column where this object '{' started in the JSON stream + */ + public int getCol() + { + return col; + } + + public int size() + { + if (containsKey("@keys")) + { + Object value = get("@keys"); + if (value instanceof Object[]) + { + return ((Object[])value).length; + } + else if (value == null) + { + return 0; + } + else + { + throw new IllegalStateException("JsonObject with @keys, but no array [] associated to it, line " + line + ", col " + col); + } + } + else if (containsKey("@items")) + { + Object value = get("@items"); + if (value instanceof Object[]) + { + return ((Object[])value).length; + } + else if (value == null) + { + return 0; + } + else + { + throw new IllegalStateException("JsonObject with @items, but no array [] associated to it, line " + line + ", col " + col); + } + } + else if (containsKey("@ref")) + { + return 0; + } + + return super.size(); + } +} diff --git a/src/main/cedarsoftware/util/io/JsonReader.java b/src/main/cedarsoftware/util/io/JsonReader.java new file mode 100644 index 0000000..526d468 --- /dev/null +++ b/src/main/cedarsoftware/util/io/JsonReader.java @@ -0,0 +1,3353 @@ +package cedarsoftware.util.io; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.Closeable; +import java.io.FilterReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.URL; +import java.sql.Timestamp; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TimeZone; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Read an object graph in JSON format and make it available in Java objects, or + * in a "Map of Maps." (untyped representation). This code handles cyclic references + * and can deserialize any Object graph without requiring a class to be 'Serializeable' + * or have any specific methods on it. It will handle classes with non public constructors. + *

+ * Usages: + *
  • + * Call the static method: {@code JsonReader.objectToJava(String json)}. This will + * return a typed Java object graph.
  • + *
  • + * Call the static method: {@code JsonReader.jsonToMaps(String json)}. This will + * return an untyped object representation of the JSON String as a Map of Maps, where + * the fields are the Map keys, and the field values are the associated Map's values. You can + * call the JsonWriter.objectToJava() method with the returned Map, and it will serialize + * the Graph into the identical JSON stream from which it was read. + *
  • + * Instantiate the JsonReader with an InputStream: {@code JsonReader(InputStream in)} and then call + * {@code readObject()}. Cast the return value of readObject() to the Java class that was the root of + * the graph. + *
  • + *
  • + * Instantiate the JsonReader with an InputStream: {@code JsonReader(InputStream in, true)} and then call + * {@code readObject()}. The return value will be a Map of Maps. + *

+ * + * @author John DeRegnaucourt (jdereg@gmail.com) + *
+ * Copyright (c) Cedar Software LLC + *

+ * 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. + */ +public class JsonReader implements Closeable +{ + private static final int STATE_READ_START_OBJECT = 0; + private static final int STATE_READ_FIELD = 1; + private static final int STATE_READ_VALUE = 2; + private static final int STATE_READ_POST_VALUE = 3; + private static final String EMPTY_ARRAY = "~!a~"; // compared with == + private static final String EMPTY_OBJECT = "~!o~"; // compared with == + private static final Character[] _charCache = new Character[128]; + private static final Byte[] _byteCache = new Byte[256]; + private static final Map _stringCache = new HashMap(); + private static final Set _prims = new HashSet(); + private static final Map _constructors = new HashMap(); + private static final Map _nameToClass = new HashMap(); + private static final Class[] _emptyClassArray = new Class[]{}; + private static final List _readers = new ArrayList(); + private static final Set _notCustom = new HashSet(); + private static final Map _months = new LinkedHashMap(); + private static final Map _factory = new LinkedHashMap(); + private static final String _mos = "(Jan|January|Feb|February|Mar|March|Apr|April|May|Jun|June|Jul|July|Aug|August|Sep|Sept|September|Oct|October|Nov|November|Dec|December)"; + private static final Pattern _datePattern1 = Pattern.compile("^(\\d{4})[\\./-](\\d{1,2})[\\./-](\\d{1,2})"); + private static final Pattern _datePattern2 = Pattern.compile("^(\\d{1,2})[\\./-](\\d{1,2})[\\./-](\\d{4})"); + private static final Pattern _datePattern3 = Pattern.compile(_mos + "[ ,]+(\\d{1,2})[ ,]+(\\d{4})", Pattern.CASE_INSENSITIVE); + private static final Pattern _datePattern4 = Pattern.compile("(\\d{1,2})[ ,]" + _mos + "[ ,]+(\\d{4})", Pattern.CASE_INSENSITIVE); + private static final Pattern _datePattern5 = Pattern.compile("(\\d{4})[ ,]" + _mos + "[ ,]+(\\d{1,2})", Pattern.CASE_INSENSITIVE); + private static final Pattern _timePattern1 = Pattern.compile("(\\d{2})[:.](\\d{2})[:.](\\d{2})[.](\\d{1,3})"); + private static final Pattern _timePattern2 = Pattern.compile("(\\d{2})[:.](\\d{2})[:.](\\d{2})"); + private static final Pattern _timePattern3 = Pattern.compile("(\\d{2})[:.](\\d{2})"); + private static final Pattern _extraQuotes = Pattern.compile("([\"]*)([^\"]*)([\"]*)"); + + private final Map _objsRead = new LinkedHashMap(); + private final Collection _unresolvedRefs = new ArrayList(); + private final Collection _prettyMaps = new ArrayList(); + private final FastPushbackReader _in; + private boolean _noObjects = false; + private final char[] _numBuf = new char[256]; + private final StringBuilder _strBuf = new StringBuilder(); + + static final ThreadLocal> _snippet = new ThreadLocal>() + { + public Deque initialValue() + { + return new ArrayDeque(128); + } + }; + static final ThreadLocal _line = new ThreadLocal() + { + public Integer initialValue() + { + return 1; + } + }; + + static final ThreadLocal _col = new ThreadLocal() + { + public Integer initialValue() + { + return 1; + } + }; + + static + { + // Save memory by re-using common Characters (Characters are immutable) + for (int i = 0; i < _charCache.length; i++) + { + _charCache[i] = (char) i; + } + + // Save memory by re-using all byte instances (Bytes are immutable) + for (int i = 0; i < _byteCache.length; i++) + { + _byteCache[i] = (byte) (i - 128); + } + + // Save heap memory by re-using common strings (String's immutable) + _stringCache.put("", ""); + _stringCache.put("true", "true"); + _stringCache.put("True", "True"); + _stringCache.put("TRUE", "TRUE"); + _stringCache.put("false", "false"); + _stringCache.put("False", "False"); + _stringCache.put("FALSE", "FALSE"); + _stringCache.put("null", "null"); + _stringCache.put("yes", "yes"); + _stringCache.put("Yes", "Yes"); + _stringCache.put("YES", "YES"); + _stringCache.put("no", "no"); + _stringCache.put("No", "No"); + _stringCache.put("NO", "NO"); + _stringCache.put("on", "on"); + _stringCache.put("On", "On"); + _stringCache.put("ON", "ON"); + _stringCache.put("off", "off"); + _stringCache.put("Off", "Off"); + _stringCache.put("OFF", "OFF"); + _stringCache.put("@id", "@id"); + _stringCache.put("@ref", "@ref"); + _stringCache.put("@items", "@items"); + _stringCache.put("@type", "@type"); + _stringCache.put("@keys", "@keys"); + _stringCache.put("0", "0"); + _stringCache.put("1", "1"); + _stringCache.put("2", "2"); + _stringCache.put("3", "3"); + _stringCache.put("4", "4"); + _stringCache.put("5", "5"); + _stringCache.put("6", "6"); + _stringCache.put("7", "7"); + _stringCache.put("8", "8"); + _stringCache.put("9", "9"); + + _prims.add(Byte.class); + _prims.add(Integer.class); + _prims.add(Long.class); + _prims.add(Double.class); + _prims.add(Character.class); + _prims.add(Float.class); + _prims.add(Boolean.class); + _prims.add(Short.class); + + _nameToClass.put("string", String.class); + _nameToClass.put("boolean", boolean.class); + _nameToClass.put("char", char.class); + _nameToClass.put("byte", byte.class); + _nameToClass.put("short", short.class); + _nameToClass.put("int", int.class); + _nameToClass.put("long", long.class); + _nameToClass.put("float", float.class); + _nameToClass.put("double", double.class); + _nameToClass.put("date", Date.class); + _nameToClass.put("class", Class.class); + + addReader(String.class, new StringReader()); + addReader(Date.class, new DateReader()); + addReader(BigInteger.class, new BigIntegerReader()); + addReader(BigDecimal.class, new BigDecimalReader()); + addReader(java.sql.Date.class, new SqlDateReader()); + addReader(Timestamp.class, new TimestampReader()); + addReader(Calendar.class, new CalendarReader()); + addReader(TimeZone.class, new TimeZoneReader()); + addReader(Locale.class, new LocaleReader()); + addReader(Class.class, new ClassReader()); + addReader(StringBuilder.class, new StringBuilderReader()); + addReader(StringBuffer.class, new StringBufferReader()); + + ClassFactory colFactory = new CollectionFactory(); + assignInstantiator(Collection.class, colFactory); + assignInstantiator(List.class, colFactory); + assignInstantiator(Set.class, colFactory); + assignInstantiator(SortedSet.class, colFactory); + assignInstantiator(Collection.class, colFactory); + + ClassFactory mapFactory = new MapFactory(); + assignInstantiator(Map.class, mapFactory); + assignInstantiator(SortedMap.class, mapFactory); + + // Month name to number map + _months.put("jan", "1"); + _months.put("january", "1"); + _months.put("feb", "2"); + _months.put("february", "2"); + _months.put("mar", "3"); + _months.put("march", "3"); + _months.put("apr", "4"); + _months.put("april", "4"); + _months.put("may", "5"); + _months.put("jun", "6"); + _months.put("june", "6"); + _months.put("jul", "7"); + _months.put("july", "7"); + _months.put("aug", "8"); + _months.put("august", "8"); + _months.put("sep", "9"); + _months.put("sept", "9"); + _months.put("september", "9"); + _months.put("oct", "10"); + _months.put("october", "10"); + _months.put("nov", "11"); + _months.put("november", "11"); + _months.put("dec", "12"); + _months.put("december", "12"); + } + + public interface JsonClassReader + { + Object read(Object jOb, LinkedList> stack) throws IOException; + } + + public interface ClassFactory + { + Object newInstance(Class c); + } + + /** + * For difficult to instantiate classes, you can add your own ClassFactory + * which will be called when the passed in class 'c' is encountered. Your + * ClassFactory will be called with newInstance(c) and your factory is expected + * to return a new instance of 'c'. + * + * This API is an 'escape hatch' to allow ANY object to be instantiated by JsonReader + * and is useful when you encounter a class that JsonReader cannot instantiate using its + * internal exhausting attempts (trying all constructors, varying arguments to them, etc.) + */ + public static void assignInstantiator(Class c, ClassFactory factory) + { + _factory.put(c, factory); + } + + /** + * Use to create new instances of collection interfaces (needed for empty collections) + */ + public static class CollectionFactory implements ClassFactory + { + public Object newInstance(Class c) + { + if (List.class.isAssignableFrom(c)) + { + return new ArrayList(); + } + else if (SortedSet.class.isAssignableFrom(c)) + { + return new TreeSet(); + } + else if (Set.class.isAssignableFrom(c)) + { + return new LinkedHashSet(); + } + else if (Collection.class.isAssignableFrom(c)) + { + return new ArrayList(); + } + throw new RuntimeException("CollectionFactory handed Class for which it was not expecting: " + c.getName()); + } + } + + /** + * Use to create new instances of Map interfaces (needed for empty Maps) + */ + public static class MapFactory implements ClassFactory + { + public Object newInstance(Class c) + { + if (SortedMap.class.isAssignableFrom(c)) + { + return new TreeMap(); + } + else if (Map.class.isAssignableFrom(c)) + { + return new LinkedHashMap(); + } + throw new RuntimeException("MapFactory handed Class for which it was not expecting: " + c.getName()); + } + } + + public static class TimeZoneReader implements JsonClassReader + { + public Object read(Object o, LinkedList> stack) throws IOException + { + JsonObject jObj = (JsonObject)o; + Object zone = jObj.get("zone"); + if (zone == null) + { + error("java.util.TimeZone must specify 'zone' field"); + } + return jObj.target = TimeZone.getTimeZone((String) zone); + } + } + + public static class LocaleReader implements JsonClassReader + { + public Object read(Object o, LinkedList> stack) throws IOException + { + JsonObject jObj = (JsonObject) o; + Object language = jObj.get("language"); + if (language == null) + { + error("java.util.Locale must specify 'language' field"); + } + Object country = jObj.get("country"); + Object variant = jObj.get("variant"); + if (country == null) + { + return jObj.target = new Locale((String) language); + } + if (variant == null) + { + return jObj.target = new Locale((String) language, (String) country); + } + + return jObj.target = new Locale((String) language, (String) country, (String) variant); + } + } + + public static class CalendarReader implements JsonClassReader + { + public Object read(Object o, LinkedList> stack) throws IOException + { + String time = null; + try + { + JsonObject jObj = (JsonObject) o; + time = (String) jObj.get("time"); + if (time == null) + { + error("Calendar missing 'time' field"); + } + Date date = JsonWriter._dateFormat.get().parse(time); + Class c; + if (jObj.getTarget() != null) + { + c = jObj.getTarget().getClass(); + } + else + { + Object type = jObj.type; + c = classForName2((String) type); + } + + Calendar calendar = (Calendar) newInstance(c); + calendar.setTime(date); + jObj.setTarget(calendar); + String zone = (String) jObj.get("zone"); + if (zone != null) + { + calendar.setTimeZone(TimeZone.getTimeZone(zone)); + } + return calendar; + } + catch(Exception e) + { + return error("Failed to parse calendar, time: " + time); + } + } + } + + public static class DateReader implements JsonClassReader + { + public Object read(Object o, LinkedList> stack) throws IOException + { + if (o instanceof Long) + { + return new Date((Long) o); + } + else if (o instanceof String) + { + return parseDate((String)o); + } + else if (o instanceof JsonObject) + { + JsonObject jObj = (JsonObject)o; + Object val = jObj.get("value"); + if (val instanceof Long) + { + return new Date((Long) val); + } + else if (val instanceof String) + { + return parseDate((String) val); + } + return error("Unable to parse date: " + o); + } + else + { + return error("Unable to parse date, encountered unknown object: " + o); + } + } + + private static Date parseDate(String dateStr) throws IOException + { + dateStr = dateStr.trim(); + if ("".equals(dateStr)) + { + return null; + } + + // Determine which date pattern (Matcher) to use + Matcher matcher = _datePattern1.matcher(dateStr); + + String year, month = null, day, mon = null; + + if (matcher.find()) + { + year = matcher.group(1); + month = matcher.group(2); + day = matcher.group(3); + } + else + { + matcher = _datePattern2.matcher(dateStr); + if (matcher.find()) + { + month = matcher.group(1); + day = matcher.group(2); + year = matcher.group(3); + } + else + { + matcher = _datePattern3.matcher(dateStr); + if (matcher.find()) + { + mon = matcher.group(1); + day = matcher.group(2); + year = matcher.group(3); + } + else + { + matcher = _datePattern4.matcher(dateStr); + if (matcher.find()) + { + day = matcher.group(1); + mon = matcher.group(2); + year = matcher.group(3); + } + else + { + matcher = _datePattern5.matcher(dateStr); + if (!matcher.find()) + { + error("Unable to parse: " + dateStr); + } + year = matcher.group(1); + mon = matcher.group(2); + day = matcher.group(3); + } + } + } + } + + if (mon != null) + { + month = _months.get(mon.trim().toLowerCase()); + if (month == null) + { + error("Unable to parse month portion of date: " + dateStr); + } + } + + // Determine which date pattern (Matcher) to use + matcher = _timePattern1.matcher(dateStr); + if (!matcher.find()) + { + matcher = _timePattern2.matcher(dateStr); + if (!matcher.find()) + { + matcher = _timePattern3.matcher(dateStr); + if (!matcher.find()) + { + matcher = null; + } + } + } + + Calendar c = Calendar.getInstance(); + c.clear(); + + // Always parses correctly because of regex. + int y = Integer.parseInt(year); + int m = Integer.parseInt(month) - 1; // months are 0-based + int d = Integer.parseInt(day); + + if (m < 0 || m > 11) + { + error("Month must be between 1 and 12, date: " + dateStr); + } + if (d < 0 || d > 31) + { + error("Day cannot be > 31, date: " + dateStr); + } + + if (matcher == null) + { // no [valid] time portion + c.set(y, m, d); + } + else + { + String hour = matcher.group(1); + String min = matcher.group(2); + String sec = "00"; + String milli = "000"; + if (matcher.groupCount() > 2) + { + sec = matcher.group(3); + } + if (matcher.groupCount() > 3) + { + milli = matcher.group(4); + } + + int h = Integer.parseInt(hour); + int mn = Integer.parseInt(min); + int s = Integer.parseInt(sec); + int ms = Integer.parseInt(milli); + + if (h < 0 || h > 23) + { + error("Hour must be between 0 and 23, time: " + dateStr); + } + if (mn < 0 || mn > 59) + { + error("Minute must be between 0 and 59, time: " + dateStr); + } + if (s < 0 || s > 59) + { + error("Second must be between 0 and 59, time: " + dateStr); + } + + c.set(y, m, d, h, mn, s); + c.set(Calendar.MILLISECOND, ms); + } + return c.getTime(); + } + } + + public static class SqlDateReader extends DateReader + { + public Object read(Object o, LinkedList> stack) throws IOException + { + return new java.sql.Date(((Date) super.read(o, stack)).getTime()); + } + } + + public static class StringReader implements JsonClassReader + { + public Object read(Object o, LinkedList> stack) throws IOException + { + if (o instanceof String) + { + return o; + } + + if (isPrimitive(o.getClass())) + { + return o.toString(); + } + + JsonObject jObj = (JsonObject) o; + if (jObj.containsKey("value")) + { + return jObj.target = jObj.get("value"); + } + return error("String missing 'value' field"); + } + } + + public static class ClassReader implements JsonClassReader + { + public Object read(Object o, LinkedList> stack) throws IOException + { + if (o instanceof String) + { + return classForName2((String)o); + } + + JsonObject jObj = (JsonObject) o; + if (jObj.containsKey("value")) + { + return jObj.target = classForName2((String) jObj.get("value")); + } + return error("Class missing 'value' field"); + } + } + + public static class BigIntegerReader implements JsonClassReader + { + public Object read(Object o, LinkedList> stack) throws IOException + { + JsonObject jObj = null; + Object value = o; + if (o instanceof JsonObject) + { + jObj = (JsonObject) o; + if (jObj.containsKey("value")) + { + value = jObj.get("value"); + } + else + { + return error("BigInteger missing 'value' field"); + } + } + + if (value instanceof JsonObject) + { + JsonObject valueObj = (JsonObject)value; + if ("java.math.BigDecimal".equals(valueObj.type)) + { + BigDecimalReader reader = new BigDecimalReader(); + value = reader.read(value, stack); + } + else if ("java.math.BigInteger".equals(valueObj.type)) + { + value = read(value, stack); + } + else + { + return error("Unknown object type attempted to be assigned to BigInteger field: " + value); + } + } + + BigInteger x = bigIntegerFrom(value); + if (jObj != null) + { + jObj.target = x; + } + + return x; + } + } + + /** + * @return a BigInteger from the given input. A best attempt will be made to support + * as many input types as possible. For example, if the input is a Boolean, a BigInteger of + * 1 or 0 will be returned. If the input is a String "", a null will be returned. If the + * input is a Double, Float, or BigDecimal, a BigInteger will be returned that retains the + * integer portion (fractional part is dropped). The input can be a Byte, Short, Integer, + * or Long. + * @throws IOException if the input is something that cannot be converted to a BigInteger. + */ + public static BigInteger bigIntegerFrom(Object value) throws IOException + { + if (value == null) + { + return null; + } + else if (value instanceof BigInteger) + { + return (BigInteger) value; + } + else if (value instanceof String) + { + String s = (String) value; + if ("".equals(s.trim())) + { // Allows "" to be used to assign null to BigInteger field. + return null; + } + try + { + return new BigInteger(removeLeadingAndTrailingQuotes(s)); + } + catch (Exception e) + { + return (BigInteger) error("Could not parse '" + value + "' as BigInteger.", e); + } + } + else if (value instanceof BigDecimal) + { + BigDecimal bd = (BigDecimal) value; + return bd.toBigInteger(); + } + else if (value instanceof Boolean) + { + return new BigInteger(((Boolean) value) ? "1" : "0"); + } + else if (value instanceof Double || value instanceof Float) + { + return new BigDecimal(((Number)value).doubleValue()).toBigInteger(); + } + else if (value instanceof Long || value instanceof Integer || + value instanceof Short || value instanceof Byte) + { + return new BigInteger(value.toString()); + } + return (BigInteger) error("Could not convert value: " + value.toString() + " to BigInteger."); + } + + public static class BigDecimalReader implements JsonClassReader + { + public Object read(Object o, LinkedList> stack) throws IOException + { + JsonObject jObj = null; + Object value = o; + if (o instanceof JsonObject) + { + jObj = (JsonObject) o; + if (jObj.containsKey("value")) + { + value = jObj.get("value"); + } + else + { + return error("BigDecimal missing 'value' field"); + } + } + + if (value instanceof JsonObject) + { + JsonObject valueObj = (JsonObject)value; + if ("java.math.BigInteger".equals(valueObj.type)) + { + BigIntegerReader reader = new BigIntegerReader(); + value = reader.read(value, stack); + } + else if ("java.math.BigDecimal".equals(valueObj.type)) + { + value = read(value, stack); + } + else + { + return error("Unknown object type attempted to be assigned to BigInteger field: " + value); + } + } + + BigDecimal x = bigDecimalFrom(value); + if (jObj != null) + { + jObj.target = x; + } + return x; + } + } + + /** + * @return a BigDecimal from the given input. A best attempt will be made to support + * as many input types as possible. For example, if the input is a Boolean, a BigDecimal of + * 1 or 0 will be returned. If the input is a String "", a null will be returned. The input + * can be a Byte, Short, Integer, Long, or BigInteger. + * @throws IOException if the input is something that cannot be converted to a BigDecimal. + */ + public static BigDecimal bigDecimalFrom(Object value) throws IOException + { + if (value == null) + { + return null; + } + else if (value instanceof BigDecimal) + { + return (BigDecimal) value; + } + else if (value instanceof String) + { + String s = (String) value; + if ("".equals(s.trim())) + { + return null; + } + try + { + return new BigDecimal(removeLeadingAndTrailingQuotes(s)); + } + catch (Exception e) + { + return (BigDecimal) error("Could not parse '" + s + "' as BigDecimal.", e); + } + } + else if (value instanceof BigInteger) + { + return new BigDecimal((BigInteger) value); + } + else if (value instanceof Boolean) + { + return new BigDecimal(((Boolean) value) ? "1" : "0"); + } + else if (value instanceof Long || value instanceof Integer || value instanceof Double || + value instanceof Short || value instanceof Byte || value instanceof Float) + { + return new BigDecimal(value.toString()); + } + return (BigDecimal) error("Could not convert value: " + value.toString() + " to BigInteger."); + } + + public static class StringBuilderReader implements JsonClassReader + { + public Object read(Object o, LinkedList> stack) throws IOException + { + if (o instanceof String) + { + return new StringBuilder((String) o); + } + + JsonObject jObj = (JsonObject) o; + if (jObj.containsKey("value")) + { + return jObj.target = new StringBuilder((String) jObj.get("value")); + } + return error("StringBuilder missing 'value' field"); + } + } + + public static class StringBufferReader implements JsonClassReader + { + public Object read(Object o, LinkedList> stack) throws IOException + { + if (o instanceof String) + { + return new StringBuffer((String) o); + } + + JsonObject jObj = (JsonObject) o; + if (jObj.containsKey("value")) + { + return jObj.target = new StringBuffer((String) jObj.get("value")); + } + return error("StringBuffer missing 'value' field"); + } + } + + public static class TimestampReader implements JsonClassReader + { + public Object read(Object o, LinkedList> stack) throws IOException + { + JsonObject jObj = (JsonObject) o; + Object time = jObj.get("time"); + if (time == null) + { + error("java.sql.Timestamp must specify 'time' field"); + } + Object nanos = jObj.get("nanos"); + if (nanos == null) + { + return jObj.target = new Timestamp(Long.valueOf((String) time)); + } + + Timestamp tstamp = new Timestamp(Long.valueOf((String) time)); + tstamp.setNanos(Integer.valueOf((String) nanos)); + return jObj.target = tstamp; + } + } + + public static void addReader(Class c, JsonClassReader reader) + { + for (Object[] item : _readers) + { + Class clz = (Class)item[0]; + if (clz == c) + { + item[1] = reader; // Replace reader + return; + } + } + _readers.add(new Object[] {c, reader}); + } + + public static void addNotCustomReader(Class c) + { + _notCustom.add(c); + } + + protected Object readIfMatching(Object o, Class compType, LinkedList> stack) throws IOException + { + if (o == null) + { + error("Bug in json-io, null must be checked before calling this method."); + } + + if (_notCustom.contains(o.getClass())) + { + return null; + } + + if (compType != null) + { + if (_notCustom.contains(compType)) + { + return null; + } + } + + boolean isJsonObject = o instanceof JsonObject; + if (!isJsonObject && compType == null) + { // If not a JsonObject (like a Long that represents a date, then compType must be set) + return null; + } + + Class c; + boolean needsType = false; + + // Set up class type to check against reader classes (specified as @type, or jObj.target, or compType) + if (isJsonObject) + { + JsonObject jObj = (JsonObject) o; + if (jObj.containsKey("@ref")) + { + return null; + } + + if (jObj.target == null) + { // '@type' parameter used + String typeStr = null; + try + { + Object type = jObj.type; + if (type != null) + { + typeStr = (String) type; + c = classForName((String) type); + } + else + { + if (compType != null) + { + c = compType; + needsType = true; + } + else + { + return null; + } + } + } + catch(Exception e) + { + return error("Class listed in @type [" + typeStr + "] is not found", e); + } + } + else + { // Type inferred from target object + c = jObj.target.getClass(); + } + } + else + { + c = compType; + } + + JsonClassReader closestReader = null; + int minDistance = Integer.MAX_VALUE; + + for (Object[] item : _readers) + { + Class clz = (Class)item[0]; + if (clz == c) + { + closestReader = (JsonClassReader)item[1]; + break; + } + int distance = JsonWriter.getDistance(clz, c); + if (distance < minDistance) + { + minDistance = distance; + closestReader = (JsonClassReader)item[1]; + } + } + + if (closestReader == null) + { + return null; + } + + if (needsType && isJsonObject) + { + ((JsonObject)o).setType(c.getName()); + } + return closestReader.read(o, stack); + } + + /** + * UnresolvedReference is created to hold a logical pointer to a reference that + * could not yet be loaded, as the @ref appears ahead of the referenced object's + * definition. This can point to a field reference or an array/Collection element reference. + */ + private static class UnresolvedReference + { + private JsonObject referencingObj; + private String field; + private long refId; + private int index = -1; + + private UnresolvedReference(JsonObject referrer, String fld, long id) + { + referencingObj = referrer; + field = fld; + refId = id; + } + + private UnresolvedReference(JsonObject referrer, int idx, long id) + { + referencingObj = referrer; + index = idx; + refId = id; + } + } + + /** + * Convert the passed in JSON string into a Java object graph. + * + * @param json String JSON input + * @return Java object graph matching JSON input, or null if an + * error occurred. + */ + @Deprecated + public static Object toJava(String json) + { + throw new RuntimeException("Use com.cedarsoftware.util.JsonReader.jsonToJava()"); + } + + /** + * Convert the passed in JSON string into a Java object graph. + * + * @param json String JSON input + * @return Java object graph matching JSON input + * @throws java.io.IOException If an I/O error occurs + */ + public static Object jsonToJava(String json) throws IOException + { + ByteArrayInputStream ba = new ByteArrayInputStream(json.getBytes("UTF-8")); + JsonReader jr = new JsonReader(ba, false); + Object obj = jr.readObject(); + jr.close(); + return obj; + } + + /** + * Convert the passed in JSON string into a Java object graph + * that consists solely of Java Maps where the keys are the + * fields and the values are primitives or other Maps (in the + * case of objects). + * + * @param json String JSON input + * @return Java object graph of Maps matching JSON input, + * or null if an error occurred. + */ + @Deprecated + public static Map toMaps(String json) + { + throw new RuntimeException("Use com.cedarsoftware.util.JsonReader.jsonToMaps()"); + } + + /** + * Convert the passed in JSON string into a Java object graph + * that consists solely of Java Maps where the keys are the + * fields and the values are primitives or other Maps (in the + * case of objects). + * + * @param json String JSON input + * @return Java object graph of Maps matching JSON input, + * or null if an error occurred. + * @throws java.io.IOException If an I/O error occurs + */ + public static Map jsonToMaps(String json) throws IOException + { + ByteArrayInputStream ba = new ByteArrayInputStream(json.getBytes("UTF-8")); + JsonReader jr = new JsonReader(ba, true); + Map map = (Map) jr.readObject(); + jr.close(); + return map; + } + + public JsonReader() + { + _noObjects = false; + _in = null; + } + + public JsonReader(InputStream in) + { + this(in, false); + } + + public JsonReader(InputStream in, boolean noObjects) + { + _noObjects = noObjects; + try + { + _in = new FastPushbackReader(new BufferedReader(new InputStreamReader(in, "UTF-8"))); + } + catch (UnsupportedEncodingException e) + { + throw new RuntimeException("Your JVM does not support UTF-8. Get a new JVM.", e); + } + } + + /** + * Finite State Machine (FSM) used to parse the JSON input into + * JsonObject's (Maps). Then, if requested, the JsonObjects are + * converted into Java instances. + * + * @return Java Object graph constructed from InputStream supplying + * JSON serialized content. + * @throws IOException for stream errors or parsing errors. + */ + public Object readObject() throws IOException + { + Object o = readJsonObject(); + if (o == EMPTY_OBJECT) + { + return new JsonObject(); + } + + Object graph = convertParsedMapsToJava((JsonObject) o); + + // Allow a complete 'Map' return (Javascript style) + if (_noObjects) + { + return o; + } + return graph; + } + + /** + * Convert a root JsonObject that represents parsed JSON, into + * an actual Java object. + * @param root JsonObject instance that was the root object from the + * JSON input that was parsed in an earlier call to JsonReader. + * @return a typed Java instance that was serialized into JSON. + */ + public Object jsonObjectsToJava(JsonObject root) throws IOException + { + _noObjects = false; + return convertParsedMapsToJava(root); + } + + /** + * This method converts a root Map, (which contains nested Maps + * and so forth representing a Java Object graph), to a Java + * object instance. The root map came from using the JsonReader + * to parse a JSON graph (using the API that puts the graph + * into Maps, not the typed representation). + * @param root JsonObject instance that was the root object from the + * JSON input that was parsed in an earlier call to JsonReader. + * @return a typed Java instance that was serialized into JSON. + */ + private Object convertParsedMapsToJava(JsonObject root) throws IOException + { + createJavaObjectInstance(Object.class, root); + Object graph = convertMapsToObjects((JsonObject) root); + patchUnresolvedReferences(); + rehashMaps(); + _objsRead.clear(); + _unresolvedRefs.clear(); + _prettyMaps.clear(); + return graph; + } + + /** + * Walk a JsonObject (Map of String keys to values) and return the + * Java object equivalent filled in as best as possible (everything + * except unresolved reference fields or unresolved array/collection elements). + * + * @param root JsonObject reference to a Map-of-Maps representation of the JSON + * input after it has been completely read. + * @return Properly constructed, typed, Java object graph built from a Map + * of Maps representation (JsonObject root). + * @throws IOException for stream errors or parsing errors. + */ + private Object convertMapsToObjects(JsonObject root) throws IOException + { + LinkedList> stack = new LinkedList>(); + stack.addFirst(root); + final boolean useMaps = _noObjects; + + while (!stack.isEmpty()) + { + JsonObject jsonObj = stack.removeFirst(); + + if (useMaps) + { + if (jsonObj.isArray() || jsonObj.isCollection()) + { + traverseCollectionNoObj(stack, jsonObj); + } + else if (jsonObj.isMap()) + { + traverseMap(stack, jsonObj); + } + else + { + traverseFieldsNoObj(stack, jsonObj); + } + } + else + { + if (jsonObj.isArray()) + { + traverseArray(stack, jsonObj); + } + else if (jsonObj.isCollection()) + { + traverseCollection(stack, jsonObj); + } + else if (jsonObj.isMap()) + { + traverseMap(stack, jsonObj); + } + else + { + traverseFields(stack, jsonObj); + } + + // Reduce heap footprint during processing + jsonObj.clear(); + } + } + return root.target; + } + + /** + * Traverse the JsonObject associated to an array (of any type). Convert and + * assign the list of items in the JsonObject (stored in the @items field) + * to each array element. All array elements are processed excluding elements + * that reference an unresolved object. These are filled in later. + * + * @param stack a Stack (LinkedList) used to support graph traversal. + * @param jsonObj a Map-of-Map representation of the JSON input stream. + * @throws IOException for stream errors or parsing errors. + */ + private void traverseArray(LinkedList> stack, JsonObject jsonObj) throws IOException + { + int len = jsonObj.getLength(); + if (len == 0) + { + return; + } + + Class compType = jsonObj.getComponentType(); + + if (char.class == compType) + { + return; + } + + if (byte.class == compType) + { // Handle byte[] special for performance boost. + jsonObj.moveBytesToMate(); + jsonObj.clearArray(); + return; + } + + boolean isPrimitive = isPrimitive(compType); + Object array = jsonObj.target; + Object[] items = jsonObj.getArray(); + + for (int i = 0; i < len; i++) + { + Object element = items[i]; + + Object special; + if (element == null) + { + Array.set(array, i, null); + } + else if (element == EMPTY_OBJECT) + { // Use either explicitly defined type in ObjectMap associated to JSON, or array component type. + Object arrayElement = createJavaObjectInstance(compType, new JsonObject()); + Array.set(array, i, arrayElement); + } + else if ((special = readIfMatching(element, compType, stack)) != null) + { + Array.set(array, i, special); + } + else if (isPrimitive) + { // Primitive component type array + Array.set(array, i, newPrimitiveWrapper(compType, element)); + } + else if (element.getClass().isArray()) + { // Array of arrays + if (char[].class == compType) + { // Specially handle char[] because we are writing these + // out as UTF-8 strings for compactness and speed. + Object[] jsonArray = (Object[]) element; + if (jsonArray.length == 0) + { + Array.set(array, i, new char[]{}); + } + else + { + String value = (String) jsonArray[0]; + int numChars = value.length(); + char[] chars = new char[numChars]; + for (int j = 0; j < numChars; j++) + { + chars[j] = value.charAt(j); + } + Array.set(array, i, chars); + } + } + else + { + JsonObject jsonObject = new JsonObject(); + jsonObject.put("@items", element); + Array.set(array, i, createJavaObjectInstance(compType, jsonObject)); + stack.addFirst(jsonObject); + } + } + else if (element instanceof JsonObject) + { + JsonObject jsonObject = (JsonObject) element; + Long ref = (Long) jsonObject.get("@ref"); + + if (ref != null) + { // Connect reference + JsonObject refObject = _objsRead.get(ref); + if (refObject == null) + { + error("Forward reference @ref: " + ref + ", but no object defined (@id) with that value"); + } + if (refObject.target != null) + { // Array element with @ref to existing object + Array.set(array, i, refObject.target); + } + else + { // Array with a forward @ref as an element + _unresolvedRefs.add(new UnresolvedReference(jsonObj, i, ref)); + } + } + else + { // Convert JSON HashMap to Java Object instance and assign values + Object arrayElement = createJavaObjectInstance(compType, jsonObject); + Array.set(array, i, arrayElement); + if (!isPrimitive(arrayElement.getClass())) + { // Skip walking primitives, primitive wrapper classes,, Strings, and Classes + stack.addFirst(jsonObject); + } + } + } + else + { + if (element instanceof String && "".equals(((String) element).trim()) && compType != String.class && compType != Object.class) + { // Allow an entry of "" in the array to set the array element to null, *if* the array type is NOT String[] and NOT Object[] + Array.set(array, i, null); + } + else + { + Array.set(array, i, element); + } + } + } + jsonObj.clearArray(); + } + + /** + * Process java.util.Collection and it's derivatives. Collections are written specially + * so that the serialization does not expose the Collection's internal structure, for + * example a TreeSet. All entries are processed, except unresolved references, which + * are filled in later. For an indexable collection, the unresolved references are set + * back into the proper element location. For non-indexable collections (Sets), the + * unresolved references are added via .add(). + * @param stack a Stack (LinkedList) used to support graph traversal. + * @param jsonObj a Map-of-Map representation of the JSON input stream. + * @throws IOException for stream errors or parsing errors. + */ + private void traverseCollectionNoObj(LinkedList> stack, JsonObject jsonObj) throws IOException + { + Object[] items = jsonObj.getArray(); + if (items == null || items.length == 0) + { + return; + } + + int idx = 0; + List copy = new ArrayList(items.length); + + for (Object element : items) + { + if (element == EMPTY_OBJECT) + { + copy.add(new JsonObject()); + continue; + } + + copy.add(element); + + if (element instanceof Object[]) + { // array element inside Collection + JsonObject jsonObject = new JsonObject(); + jsonObject.put("@items", element); + stack.addFirst(jsonObject); + } + else if (element instanceof JsonObject) + { + JsonObject jsonObject = (JsonObject) element; + Long ref = (Long) jsonObject.get("@ref"); + + if (ref != null) + { // connect reference + JsonObject refObject = _objsRead.get(ref); + if (refObject == null) + { + error("Forward reference @ref: " + ref + ", but no object defined (@id) with that value"); + } + copy.set(idx, refObject); + } + else + { + stack.addFirst(jsonObject); + } + } + idx++; + } + jsonObj.target = null; // don't waste space (used for typed return, not generic Map return) + + for (int i=0; i < items.length; i++) + { + items[i] = copy.get(i); + } + } + + /** + * Process java.util.Collection and it's derivatives. Collections are written specially + * so that the serialization does not expose the Collection's internal structure, for + * example a TreeSet. All entries are processed, except unresolved references, which + * are filled in later. For an indexable collection, the unresolved references are set + * back into the proper element location. For non-indexable collections (Sets), the + * unresolved references are added via .add(). + * @param jsonObj a Map-of-Map representation of the JSON input stream. + * @throws IOException for stream errors or parsing errors. + */ + private void traverseCollection(LinkedList> stack, JsonObject jsonObj) throws IOException + { + Object[] items = jsonObj.getArray(); + if (items == null || items.length == 0) + { + return; + } + Collection col = (Collection) jsonObj.target; + boolean isList = col instanceof List; + int idx = 0; + + for (Object element : items) + { + Object special; + if (element == null) + { + col.add(null); + } + else if (element == EMPTY_OBJECT) + { // Handles {} + col.add(new JsonObject()); + } + else if ((special = readIfMatching(element, null, stack)) != null) + { + col.add(special); + } + else if (element instanceof String || element instanceof Boolean || element instanceof Double || element instanceof Long) + { // Allow Strings, Booleans, Longs, and Doubles to be "inline" without Java object decoration (@id, @type, etc.) + col.add(element); + } + else if (element.getClass().isArray()) + { + JsonObject jObj = new JsonObject(); + jObj.put("@items", element); + createJavaObjectInstance(Object.class, jObj); + col.add(jObj.target); + convertMapsToObjects(jObj); + } + else // if (element instanceof JsonObject) + { + JsonObject jObj = (JsonObject) element; + Long ref = (Long) jObj.get("@ref"); + + if (ref != null) + { + JsonObject refObject = _objsRead.get(ref); + if (refObject == null) + { + error("Forward reference @ref: " + ref + ", but no object defined (@id) with that value"); + } + + if (refObject.target != null) + { + col.add(refObject.target); + } + else + { + _unresolvedRefs.add(new UnresolvedReference(jsonObj, idx, ref)); + if (isList) + { // Indexable collection, so set 'null' as element for now - will be patched in later. + col.add(null); + } + } + } + else + { + createJavaObjectInstance(Object.class, jObj); + + if (!isPrimitive(jObj.getTargetClass())) + { + convertMapsToObjects(jObj); + } + col.add(jObj.target); + } + } + idx++; + } + + jsonObj.remove("@items"); // Reduce memory required during processing + } + + /** + * Process java.util.Map and it's derivatives. These can be written specially + * so that the serialization would not expose the derivative class internals + * (internal fields of TreeMap for example). + * @param stack a Stack (LinkedList) used to support graph traversal. + * @param jsonObj a Map-of-Map representation of the JSON input stream. + * @throws IOException for stream errors or parsing errors. + */ + private void traverseMap(LinkedList> stack, JsonObject jsonObj) throws IOException + { + // Convert @keys to a Collection of Java objects. + convertMapToKeysItems(jsonObj); + Object[] keys = (Object[]) jsonObj.get("@keys"); + Object[] items = jsonObj.getArray(); + + if (keys == null || items == null) + { + if (keys != items) + { + error("Map written where one of @keys or @items is empty"); + } + return; + } + + int size = keys.length; + if (size != items.length) + { + error("Map written with @keys and @items entries of different sizes"); + } + + JsonObject jsonKeyCollection = new JsonObject(); + jsonKeyCollection.put("@items", keys); + Object[] javaKeys = new Object[size]; + jsonKeyCollection.target = javaKeys; + stack.addFirst(jsonKeyCollection); + + // Convert @items to a Collection of Java objects. + JsonObject jsonItemCollection = new JsonObject(); + jsonItemCollection.put("@items", items); + Object[] javaValues = new Object[size]; + jsonItemCollection.target = javaValues; + stack.addFirst(jsonItemCollection); + + // Save these for later so that unresolved references inside keys or values + // get patched first, and then build the Maps. + _prettyMaps.add(new Object[] {jsonObj, javaKeys, javaValues}); + } + + private void traverseFieldsNoObj(LinkedList> stack, JsonObject jsonObj) throws IOException + { + final Object target = jsonObj.target; + for (Map.Entry e : jsonObj.entrySet()) + { + String key = e.getKey(); + + if (key.charAt(0) == '@') + { // Skip our own meta fields + continue; + } + + Field field = null; + if (target != null) + { + field = getDeclaredField(target.getClass(), key); + } + + Object value = e.getValue(); + + if (value == null) + { + jsonObj.put(key, null); + } + else if (value == EMPTY_OBJECT) + { + jsonObj.put(key, new JsonObject()); + } + else if (value.getClass().isArray()) + { // LHS of assignment is an [] field or RHS is an array and LHS is Object (Map) + JsonObject jsonArray = new JsonObject(); + jsonArray.put("@items", value); + stack.addFirst(jsonArray); + jsonObj.put(key, jsonArray); + } + else if (value instanceof JsonObject) + { + JsonObject jObj = (JsonObject) value; + if (field != null && jObj.isPrimitiveWrapper(field.getType())) + { + jObj.put("value", newPrimitiveWrapper(field.getType(),jObj.get("value"))); + continue; + } + Long ref = (Long) jObj.get("@ref"); + + if (ref != null) + { // Correct field references + JsonObject refObject = _objsRead.get(ref); + if (refObject == null) + { + error("Forward reference @ref: " + ref + ", but no object defined (@id) with that value"); + } + jsonObj.put(key, refObject); // Update Map-of-Maps reference + } + else + { + stack.addFirst(jObj); + } + } + else if (field != null) + { + final Class fieldType = field.getType(); + if (isPrimitive(fieldType)) + { + jsonObj.put(key, newPrimitiveWrapper(fieldType, value)); + } + else if (BigDecimal.class == fieldType) + { + jsonObj.put(key, bigDecimalFrom(value)); + } + else if (BigInteger.class == fieldType) + { + jsonObj.put(key, bigIntegerFrom(value)); + } + else if (value instanceof String) + { + if (fieldType != String.class && fieldType != StringBuilder.class && fieldType != StringBuffer.class) + { + if ("".equals(((String)value).trim())) + { + jsonObj.put(key, null); + } + } + } + } + } + jsonObj.target = null; // don't waste space (used for typed return, not for Map return) + } + + /** + * Walk the Java object fields and copy them from the JSON object to the Java object, performing + * any necessary conversions on primitives, or deep traversals for field assignments to other objects, + * arrays, Collections, or Maps. + * @param stack Stack (LinkedList) used for graph traversal. + * @param jsonObj a Map-of-Map representation of the current object being examined (containing all fields). + * @throws IOException + */ + private void traverseFields(LinkedList> stack, JsonObject jsonObj) throws IOException + { + Object special; + if ((special = readIfMatching(jsonObj, null, stack)) != null) + { + jsonObj.target = special; + return; + } + + Object javaMate = jsonObj.target; + Iterator> i = jsonObj.entrySet().iterator(); + Class cls = javaMate.getClass(); + + while (i.hasNext()) + { + Map.Entry e = i.next(); + String key = e.getKey(); + Field field = getDeclaredField(cls, key); + Object rhs = e.getValue(); + if (field != null) + { + assignField(stack, jsonObj, field, rhs); + } + } + jsonObj.clear(); // Reduce memory required during processing + } + + /** + * Map Json Map object field to Java object field. + * + * @param stack Stack (LinkedList) used for graph traversal. + * @param jsonObj a Map-of-Map representation of the current object being examined (containing all fields). + * @param field a Java Field object representing where the jsonObj should be converted and stored. + * @param rhs the JSON value that will be converted and stored in the 'field' on the associated + * Java target object. + * @throws IOException for stream errors or parsing errors. + */ + private void assignField(LinkedList> stack, JsonObject jsonObj, Field field, Object rhs) throws IOException + { + Object target = jsonObj.target; + try + { + Class fieldType = field.getType(); + + // If there is a "tree" of objects (e.g, Map>), the subobjects may not have an + // @type on them, if the source of the JSON is from JSON.stringify(). Deep traverse the args and + // mark @type on the items within the Maps and Collections, based on the parameterized type (if it + // exists). + if (rhs instanceof JsonObject && field.getGenericType() instanceof ParameterizedType) + { // Only JsonObject instances could contain unmarked objects. + ParameterizedType paramType = (ParameterizedType) field.getGenericType(); + markUntypedObjects(field.getGenericType(), rhs); + } + + if (rhs instanceof JsonObject) + { // Ensure .type field set on JsonObject + JsonObject job = (JsonObject) rhs; + String type = job.type; + if (type == null || type.isEmpty()) + { + job.setType(fieldType.getName()); + } + } + + Object special; + if (rhs == null) + { + field.set(target, null); + } + else if (rhs == EMPTY_OBJECT) + { + JsonObject jObj = new JsonObject(); + jObj.type = fieldType.getName(); + Object value = createJavaObjectInstance(fieldType, jObj); + field.set(target, value); + } + else if ((special = readIfMatching(rhs, field.getType(), stack)) != null) + { + field.set(target, special); + } + else if (rhs.getClass().isArray()) + { // LHS of assignment is an [] field or RHS is an array and LHS is Object + Object[] elements = (Object[]) rhs; + JsonObject jsonArray = new JsonObject(); + if (char[].class == fieldType) + { // Specially handle char[] because we are writing these + // out as UTF8 strings for compactness and speed. + if (elements.length == 0) + { + field.set(target, new char[]{}); + } + else + { + field.set(target, ((String) elements[0]).toCharArray()); + } + } + else + { + jsonArray.put("@items", elements); + createJavaObjectInstance(fieldType, jsonArray); + field.set(target, jsonArray.target); + stack.addFirst(jsonArray); + } + } + else if (rhs instanceof JsonObject) + { + JsonObject jObj = (JsonObject) rhs; + Long ref = (Long) jObj.get("@ref"); + + if (ref != null) + { // Correct field references + JsonObject refObject = _objsRead.get(ref); + if (refObject == null) + { + error("Forward reference @ref: " + ref + ", but no object defined (@id) with that value"); + } + + if (refObject.target != null) + { + field.set(target, refObject.target); + } + else + { + _unresolvedRefs.add(new UnresolvedReference(jsonObj, field.getName(), ref)); + } + } + else + { // Assign ObjectMap's to Object (or derived) fields + field.set(target, createJavaObjectInstance(fieldType, jObj)); + if (!isPrimitive(jObj.getTargetClass())) + { + stack.addFirst((JsonObject) rhs); + } + } + } + else + { + if (isPrimitive(fieldType)) + { + field.set(target, newPrimitiveWrapper(fieldType, rhs)); + } + else if (rhs instanceof String && "".equals(((String) rhs).trim()) && field.getType() != String.class) + { + field.set(target, null); + } + else + { + field.set(target, rhs); + } + } + } + catch (Exception e) + { + error("IllegalAccessException setting field '" + field.getName() + "' on target: " + target + " with value: " + rhs, e); + } + } + + private void markUntypedObjects(Type type, Object rhs) throws IOException + { + LinkedList stack = new LinkedList(); + stack.addFirst(new Object[] {type, rhs}); + + while (!stack.isEmpty()) + { + Object[] item = stack.removeFirst(); + Type t = (Type) item[0]; + if (t instanceof ParameterizedType) + { + Class clazz = getRawType(t); + ParameterizedType pType = (ParameterizedType)t; + Type[] typeArgs = pType.getActualTypeArguments(); + + if (typeArgs == null || typeArgs.length < 1 || clazz == null) + { + continue; + } + + stampTypeOnJsonObject(item[1], t); + + if (Map.class.isAssignableFrom(clazz)) + { + Map map = (Map) item[1]; + if (!map.containsKey("@keys") && !map.containsKey("@items") && map instanceof JsonObject) + { // Maps created in Javascript will come over without @keys / @items. + convertMapToKeysItems((JsonObject) map); + } + + Object[] keys = (Object[])map.get("@keys"); + getTemplateTraverseWorkItem(stack, keys, typeArgs[0]); + + Object[] items = (Object[])map.get("@items"); + getTemplateTraverseWorkItem(stack, items, typeArgs[1]); + } + else if (Collection.class.isAssignableFrom(clazz)) + { + if (item[1] instanceof Object[]) + { + Object[] array = (Object[]) item[1]; + for (int i=0; i < array.length; i++) + { + Object vals = array[i]; + stack.addFirst(new Object[]{t, vals}); + + if (vals instanceof JsonObject) + { + stack.addFirst(new Object[]{t, vals}); + } + else if (vals instanceof Object[]) + { + JsonObject coll = new JsonObject(); + coll.type = clazz.getName(); + List items = Arrays.asList((Object[]) vals); + coll.put("@items", items.toArray()); + stack.addFirst(new Object[]{t, items}); + array[i] = coll; + } + else + { + stack.addFirst(new Object[]{t, vals}); + } + } + } + else if (item[1] instanceof Collection) + { + Collection col = (Collection)item[1]; + for (Object o : col) + { + stack.addFirst(new Object[]{typeArgs[0], o}); + } + } + else if (item[1] instanceof JsonObject) + { + JsonObject jObj = (JsonObject) item[1]; + Object[] array = jObj.getArray(); + if (array != null) + { + for (Object o : array) + { + stack.addFirst(new Object[]{typeArgs[0], o}); + } + } + } + } + else + { + if (item[1] instanceof JsonObject) + { + int i=0; + for (Object o : ((JsonObject)item[1]).values()) + { + stack.addFirst(new Object[]{typeArgs[i++], o}); + } + } + } + } + else + { + stampTypeOnJsonObject(item[1], t); + } + } + } + + private void getTemplateTraverseWorkItem(LinkedList stack, Object[] items, Type type) + { + if (items == null || items.length < 1) + { + return; + } + Class rawType = getRawType(type); + if (rawType != null && Collection.class.isAssignableFrom(rawType)) + { + stack.add(new Object[]{type, items}); + } + else + { + for (Object o : items) + { + stack.add(new Object[]{type, o}); + } + } + } + + // Mark 'type' on JsonObjectwhen the type is missing and it is a 'leaf' + // node (no further subtypes in it's parameterized type definition) + private void stampTypeOnJsonObject(Object o, Type t) + { + Class clazz = t instanceof Class ? (Class)t : getRawType(t); + + if (o instanceof JsonObject && clazz != null) + { + JsonObject jObj = (JsonObject) o; + if ((jObj.type == null || jObj.type.isEmpty()) && jObj.target == null) + { + jObj.type = clazz.getName(); + } + } + } + + public static Class getRawType(Type t) + { + if (t instanceof ParameterizedType) + { + ParameterizedType pType = ((ParameterizedType) t); + + if (pType.getRawType() instanceof Class) + { + return (Class) pType.getRawType(); + } + } + return null; + } + + /** + * Convert an input JsonObject map (known to represent a Map.class or derivative) that has regular keys and values + * to have its keys placed into @keys, and its values placed into @items. + * @param map Map to convert + */ + private void convertMapToKeysItems(JsonObject map) + { + if (!map.containsKey("@keys") && !map.containsKey("@ref")) + { + Object[] keys = new Object[map.keySet().size()]; + Object[] values = new Object[map.keySet().size()]; + int i=0; + for (Object e : map.entrySet()) + { + Map.Entry entry = (Map.Entry)e; + keys[i] = entry.getKey(); + values[i] = entry.getValue(); + i++; + } + String saveType = map.getType(); + map.clear(); + map.setType(saveType); + map.put("@keys", keys); + map.put("@items", values); + } + } + + /** + * This method creates a Java Object instance based on the passed in parameters. + * If the JsonObject contains a key '@type' then that is used, as the type was explicitly + * set in the JSON stream. If the key '@type' does not exist, then the passed in Class + * is used to create the instance, handling creating an Array or regular Object + * instance. + *

+ * The '@type' is not often specified in the JSON input stream, as in many + * cases it can be inferred from a field reference or array component type. + * + * @param clazz Instance will be create of this class. + * @param jsonObj Map-of-Map representation of object to create. + * @return a new Java object of the appropriate type (clazz) using the jsonObj to provide + * enough hints to get the right class instantiated. It is not populated when returned. + * @throws IOException for stream errors or parsing errors. + */ + private Object createJavaObjectInstance(Class clazz, JsonObject jsonObj) throws IOException + { + String type = jsonObj.type; + Object mate; + + // @type always takes precedence over inferred Java (clazz) type. + if (type != null) + { // @type is explicitly set, use that as it always takes precedence + Class c = classForName(type); + if (c.isArray()) + { // Handle [] + Object[] items = jsonObj.getArray(); + int size = (items == null) ? 0 : items.length; + if (c == char[].class) + { + jsonObj.moveCharsToMate(); + mate = jsonObj.target; + } + else + { + mate = Array.newInstance(c.getComponentType(), size); + } + } + else + { // Handle regular field.object reference + if (isPrimitive(c)) + { + mate = newPrimitiveWrapper(c, jsonObj.get("value")); + } + else if (c == Class.class) + { + mate = classForName((String) jsonObj.get("value")); + } + else if (c.isEnum()) + { + mate = getEnum(c, jsonObj); + } + else if (Enum.class.isAssignableFrom(c)) // anonymous subclass of an enum + { + mate = getEnum(c.getSuperclass(), jsonObj); + } + else if ("java.util.Arrays$ArrayList".equals(c.getName())) + { // Special case: Arrays$ArrayList does not allow .add() to be called on it. + mate = new ArrayList(); + } + else + { + mate = newInstance(c); + } + } + } + else + { // @type, not specified, figure out appropriate type + Object[] items = jsonObj.getArray(); + + // if @items is specified, it must be an [] type. + // if clazz.isArray(), then it must be an [] type. + if (clazz.isArray() || (items != null && clazz == Object.class && !jsonObj.containsKey("@keys"))) + { + int size = (items == null) ? 0 : items.length; + mate = Array.newInstance(clazz.isArray() ? clazz.getComponentType() : Object.class, size); + } + else if (clazz.isEnum()) + { + mate = getEnum(clazz, jsonObj); + } + else if (Enum.class.isAssignableFrom(clazz)) // anonymous subclass of an enum + { + mate = getEnum(clazz.getSuperclass(), jsonObj); + } + else if ("java.util.Arrays$ArrayList".equals(clazz.getName())) + { // Special case: Arrays$ArrayList does not allow .add() to be called on it. + mate = new ArrayList(); + } + else if (clazz == Object.class && !_noObjects) + { + if (jsonObj.isMap() || jsonObj.size() > 0) + { + mate = new JsonObject(); + ((JsonObject)mate).type = Map.class.getName(); + } + else + { // Dunno + mate = newInstance(clazz); + } + } + else + { + mate = newInstance(clazz); + } + } + return jsonObj.target = mate; + } + + /** + * Fetch enum value (may need to try twice, due to potential 'name' field shadowing by enum subclasses + */ + private static Object getEnum(Class c, JsonObject jsonObj) + { + try + { + return Enum.valueOf(c, (String) jsonObj.get("name")); + } + catch (Exception e) + { // In case the enum class has it's own 'name' member variable (shadowing the 'name' variable on Enum) + return Enum.valueOf(c, (String) jsonObj.get("java.lang.Enum.name")); + } + } + + // Parser code + + private Object readJsonObject() throws IOException + { + boolean done = false; + String field = null; + JsonObject object = new JsonObject(); + int state = STATE_READ_START_OBJECT; + boolean objectRead = false; + final FastPushbackReader in = _in; + + while (!done) + { + int c; + switch (state) + { + case STATE_READ_START_OBJECT: + c = skipWhitespaceRead(); + if (c == '{') + { + objectRead = true; + object.line = _line.get(); + object.col = _col.get(); + c = skipWhitespaceRead(); + if (c == '}') + { // empty object + return EMPTY_OBJECT; + } + in.unread(c); + state = STATE_READ_FIELD; + } + else if (c == '[') + { + in.unread('['); + state = STATE_READ_VALUE; + } + else + { + error("Input is invalid JSON; does not start with '{' or '[', c=" + c); + } + break; + + case STATE_READ_FIELD: + c = skipWhitespaceRead(); + if (c == '"') + { + field = readString(); + c = skipWhitespaceRead(); + if (c != ':') + { + error("Expected ':' between string field and value"); + } + skipWhitespace(); + state = STATE_READ_VALUE; + } + else + { + error("Expected quote"); + } + break; + + case STATE_READ_VALUE: + if (field == null) + { // field is null when you have an untyped Object[], so we place + // the JsonArray on the @items field. + field = "@items"; + } + + Object value = readValue(object); + object.put(field, value); + + // If object is referenced (has @id), then put it in the _objsRead table. + if ("@id".equals(field)) + { + _objsRead.put((Long)value, object); + } + state = STATE_READ_POST_VALUE; + break; + + case STATE_READ_POST_VALUE: + c = skipWhitespaceRead(); + if (c == -1 && objectRead) + { + error("EOF reached before closing '}'"); + } + if (c == '}' || c == -1) + { + done = true; + } + else if (c == ',') + { + state = STATE_READ_FIELD; + } + else + { + error("Object not ended with '}' or ']'"); + } + break; + } + } + + if (_noObjects && object.isPrimitive()) + { + return object.getPrimitiveValue(); + } + + return object; + } + + private Object readValue(JsonObject object) throws IOException + { + int c = _in.read(); + + if (c == '"') + { + return readString(); + } + if (isDigit(c) || c == '-') + { + return readNumber(c); + } + if (c == '{') + { + _in.unread('{'); + return readJsonObject(); + } + if (c == 't' || c == 'T') + { + _in.unread(c); + readToken("true"); + return Boolean.TRUE; + } + if (c == 'f' || c == 'F') + { + _in.unread(c); + readToken("false"); + return Boolean.FALSE; + } + if (c == 'n' || c == 'N') + { + _in.unread(c); + readToken("null"); + return null; + } + if (c == '[') + { + return readArray(object); + } + if (c == ']') + { // [] empty array + _in.unread(']'); + return EMPTY_ARRAY; + } + if (c == -1) + { + error("EOF reached prematurely"); + } + return error("Unknown JSON value type"); + } + + /** + * Read a JSON array + */ + private Object readArray(JsonObject object) throws IOException + { + Collection array = new ArrayList(); + + while (true) + { + skipWhitespace(); + Object o = readValue(object); + if (o != EMPTY_ARRAY) + { + array.add(o); + } + int c = skipWhitespaceRead(); + + if (c == ']') + { + break; + } + if (c != ',') + { + error("Expected ',' or ']' inside array"); + } + } + + return array.toArray(); + } + + /** + * Return the specified token from the reader. If it is not found, + * throw an IOException indicating that. Converting to c to + * (char) c is acceptable because the 'tokens' allowed in a + * JSON input stream (true, false, null) are all ASCII. + */ + private String readToken(String token) throws IOException + { + int len = token.length(); + + for (int i = 0; i < len; i++) + { + int c = _in.read(); + if (c == -1) + { + error("EOF reached while reading token: " + token); + } + c = Character.toLowerCase((char) c); + int loTokenChar = token.charAt(i); + + if (loTokenChar != c) + { + error("Expected token: " + token); + } + } + + return token; + } + + /** + * Read a JSON number + * + * @param c int a character representing the first digit of the number that + * was already read. + * @return a Number (a Long or a Double) depending on whether the number is + * a decimal number or integer. This choice allows all smaller types (Float, int, short, byte) + * to be represented as well. + * @throws IOException for stream errors or parsing errors. + */ + private Number readNumber(int c) throws IOException + { + final FastPushbackReader in = _in; + final char[] numBuf = _numBuf; + numBuf[0] = (char) c; + int len = 1; + boolean isFloat = false; + + try + { + while (true) + { + c = in.read(); + if ((c >= '0' && c <= '9') || c == '-' || c == '+') // isDigit() inlined for speed here + { + numBuf[len++] = (char) c; + } + else if (c == '.' || c == 'e' || c == 'E') + { + numBuf[len++] = (char) c; + isFloat = true; + } + else if (c == -1) + { + error("Reached EOF while reading number"); + } + else + { + in.unread(c); + break; + } + } + } + catch (ArrayIndexOutOfBoundsException e) + { + error("Too many digits in number"); + } + + if (isFloat) + { // Floating point number needed + String num = new String(numBuf, 0, len); + try + { + return Double.parseDouble(num); + } + catch (NumberFormatException e) + { + error("Invalid floating point number: " + num, e); + } + } + boolean isNeg = numBuf[0] == '-'; + long n = 0; + for (int i = (isNeg ? 1 : 0); i < len; i++) + { + n = (numBuf[i] - '0') + n * 10; + } + return isNeg ? -n : n; + } + + /** + * Read a JSON string + * This method assumes the initial quote has already been read. + * + * @return String read from JSON input stream. + * @throws IOException for stream errors or parsing errors. + */ + private String readString() throws IOException + { + final StringBuilder strBuf = _strBuf; + strBuf.setLength(0); + StringBuilder hex = new StringBuilder(); + boolean done = false; + final int STATE_STRING_START = 0; + final int STATE_STRING_SLASH = 1; + final int STATE_HEX_DIGITS = 2; + int state = STATE_STRING_START; + + while (!done) + { + int c = _in.read(); + if (c == -1) + { + error("EOF reached while reading JSON string"); + } + + switch (state) + { + case STATE_STRING_START: + if (c == '\\') + { + state = STATE_STRING_SLASH; + } + else if (c == '"') + { + done = true; + } + else + { + strBuf.append(toChars(c)); + } + break; + + case STATE_STRING_SLASH: + if (c == 'n') + { + strBuf.append('\n'); + } + else if (c == 'r') + { + strBuf.append('\r'); + } + else if (c == 't') + { + strBuf.append('\t'); + } + else if (c == 'f') + { + strBuf.append('\f'); + } + else if (c == 'b') + { + strBuf.append('\b'); + } + else if (c == '\\') + { + strBuf.append('\\'); + } + else if (c == '/') + { + strBuf.append('/'); + } + else if (c == '"') + { + strBuf.append('"'); + } + else if (c == '\'') + { + strBuf.append('\''); + } + else if (c == 'u') + { + state = STATE_HEX_DIGITS; + hex.setLength(0); + break; + } + else + { + error("Invalid character escape sequence specified"); + } + state = STATE_STRING_START; + break; + + case STATE_HEX_DIGITS: + if (c == 'a' || c == 'A' || c == 'b' || c == 'B' || c == 'c' || c == 'C' || c == 'd' || c == 'D' || c == 'e' || c == 'E' || c == 'f' || c == 'F' || isDigit(c)) + { + hex.append((char) c); + if (hex.length() == 4) + { + int value = Integer.parseInt(hex.toString(), 16); + strBuf.append(valueOf((char) value)); + state = STATE_STRING_START; + } + } + else + { + error("Expected hexadecimal digits"); + } + break; + } + } + + String s = strBuf.toString(); + String cacheHit = _stringCache.get(s); + return cacheHit == null ? s : cacheHit; + } + + private static Object newInstance(Class c) throws IOException + { + if (_factory.containsKey(c)) + { + return _factory.get(c).newInstance(c); + } + + // Constructor not cached, go find a constructor + Object[] constructorInfo = _constructors.get(c); + if (constructorInfo != null) + { // Constructor was cached + Constructor constructor = (Constructor) constructorInfo[0]; + Boolean useNull = (Boolean) constructorInfo[1]; + Class[] paramTypes = constructor.getParameterTypes(); + if (paramTypes == null || paramTypes.length == 0) + { + try + { + return constructor.newInstance(); + } + catch (Exception e) + { // Should never happen, as the code that fetched the constructor was able to instantiate it once already + error("Could not instantiate " + c.getName(), e); + } + } + Object[] values = fillArgs(paramTypes, useNull); + try + { + return constructor.newInstance(values); + } + catch (Exception e) + { // Should never happen, as the code that fetched the constructor was able to instantiate it once already + error("Could not instantiate " + c.getName(), e); + } + } + + Object[] ret = newInstanceEx(c); + _constructors.put(c, new Object[] {ret[1], ret[2]}); + return ret[0]; + } + + /** + * Return constructor and instance as elements 0 and 1, respectively. + */ + private static Object[] newInstanceEx(Class c) throws IOException + { + try + { + Constructor constructor = c.getConstructor(_emptyClassArray); + if (constructor != null) + { + return new Object[] {constructor.newInstance(), constructor, true}; + } + return tryOtherConstructors(c); + } + catch (Exception e) + { + // OK, this class does not have a public no-arg constructor. Instantiate with + // first constructor found, filling in constructor values with null or + // defaults for primitives. + return tryOtherConstructors(c); + } + } + + private static Object[] tryOtherConstructors(Class c) throws IOException + { + Constructor[] constructors = c.getDeclaredConstructors(); + if (constructors.length == 0) + { + error("Cannot instantiate '" + c.getName() + "' - Primitive, interface, array[] or void"); + } + + // Try each constructor (private, protected, or public) with null values for non-primitives. + for (Constructor constructor : constructors) + { + constructor.setAccessible(true); + Class[] argTypes = constructor.getParameterTypes(); + Object[] values = fillArgs(argTypes, true); + try + { + return new Object[] {constructor.newInstance(values), constructor, true}; + } + catch (Exception ignored) + { } + } + + // Try each constructor (private, protected, or public) with non-null values for primitives. + for (Constructor constructor : constructors) + { + constructor.setAccessible(true); + Class[] argTypes = constructor.getParameterTypes(); + Object[] values = fillArgs(argTypes, false); + try + { + return new Object[] {constructor.newInstance(values), constructor, false}; + } + catch (Exception ignored) + { } + } + + error("Could not instantiate " + c.getName() + " using any constructor"); + return null; + } + + private static Object[] fillArgs(Class[] argTypes, boolean useNull) throws IOException + { + Object[] values = new Object[argTypes.length]; + for (int i = 0; i < argTypes.length; i++) + { + final Class argType = argTypes[i]; + if (isPrimitive(argType)) + { + values[i] = newPrimitiveWrapper(argType, null); + } + else if (useNull) + { + values[i] = null; + } + else + { + if (argType == String.class) + { + values[i] = ""; + } + else if (argType == Date.class) + { + values[i] = new Date(); + } + else if (List.class.isAssignableFrom(argType)) + { + values[i] = new ArrayList(); + } + else if (SortedSet.class.isAssignableFrom(argType)) + { + values[i] = new TreeSet(); + } + else if (Set.class.isAssignableFrom(argType)) + { + values[i] = new LinkedHashSet(); + } + else if (SortedMap.class.isAssignableFrom(argType)) + { + values[i] = new TreeMap(); + } + else if (Map.class.isAssignableFrom(argType)) + { + values[i] = new LinkedHashMap(); + } + else if (Collection.class.isAssignableFrom(argType)) + { + values[i] = new ArrayList(); + } + else if (Calendar.class.isAssignableFrom(argType)) + { + values[i] = Calendar.getInstance(); + } + else if (TimeZone.class.isAssignableFrom(argType)) + { + values[i] = TimeZone.getDefault(); + } + else if (argType == BigInteger.class) + { + values[i] = BigInteger.TEN; + } + else if (argType == BigDecimal.class) + { + values[i] = BigDecimal.TEN; + } + else if (argType == StringBuilder.class) + { + values[i] = new StringBuilder(); + } + else if (argType == StringBuffer.class) + { + values[i] = new StringBuffer(); + } + else if (argType == Locale.class) + { + values[i] = Locale.FRANCE; // overwritten + } + else if (argType == Class.class) + { + values[i] = String.class; + } + else if (argType == java.sql.Timestamp.class) + { + values[i] = new Timestamp(System.currentTimeMillis()); + } + else if (argType == java.sql.Date.class) + { + values[i] = new java.sql.Date(System.currentTimeMillis()); + } + else if (argType == java.net.URL.class) + { + values[i] = new URL("http://localhost"); // overwritten + } + else if (argType == Object.class) + { + values[i] = new Object(); + } + else + { + values[i] = null; + } + } + } + + return values; + } + + public static boolean isPrimitive(Class c) + { + return c.isPrimitive() || _prims.contains(c); + } + + private static Object newPrimitiveWrapper(Class c, Object rhs) throws IOException + { + if (c == Byte.class || c == byte.class) + { + if (rhs instanceof String) + { + rhs = removeLeadingAndTrailingQuotes((String) rhs); + if ("".equals(rhs)) + { + rhs = "0"; + } + return Byte.parseByte((String)rhs); + } + return rhs != null ? _byteCache[((Number) rhs).byteValue() + 128] : (byte) 0; + } + if (c == Boolean.class || c == boolean.class) + { // Booleans are tokenized into Boolean.TRUE or Boolean.FALSE + if (rhs instanceof String) + { + rhs = removeLeadingAndTrailingQuotes((String) rhs); + if ("".equals(rhs)) + { + rhs = "false"; + } + return Boolean.parseBoolean((String)rhs); + } + return rhs != null ? rhs : Boolean.FALSE; + } + if (c == Integer.class || c == int.class) + { + if (rhs instanceof String) + { + rhs = removeLeadingAndTrailingQuotes((String) rhs); + if ("".equals(rhs)) + { + rhs = "0"; + } + return Integer.parseInt((String)rhs); + } + return rhs != null ? ((Number) rhs).intValue() : 0; + } + if (c == Long.class || c == long.class || c == Number.class) + { + if (rhs instanceof String) + { + rhs = removeLeadingAndTrailingQuotes((String) rhs); + if ("".equals(rhs)) + { + rhs = "0"; + } + return Long.parseLong((String)rhs); + } + return rhs != null ? rhs : 0L; + } + if (c == Double.class || c == double.class) + { + if (rhs instanceof String) + { + rhs = removeLeadingAndTrailingQuotes((String) rhs); + if ("".equals(rhs)) + { + rhs = "0.0"; + } + return Double.parseDouble((String)rhs); + } + return rhs != null ? rhs : 0.0d; + } + if (c == Character.class || c == char.class) + { + if (rhs == null) + { + return '\u0000'; + } + if (rhs instanceof String) + { + rhs = removeLeadingAndTrailingQuotes((String) rhs); + if ("".equals(rhs)) + { + rhs = "\u0000"; + } + return valueOf(((String) rhs).charAt(0)); + } + if (rhs instanceof Character) + { + return rhs; + } + } + if (c == Short.class || c == short.class) + { + if (rhs instanceof String) + { + rhs = removeLeadingAndTrailingQuotes((String) rhs); + if ("".equals(rhs)) + { + rhs = "0"; + } + return Short.parseShort((String)rhs); + } + return rhs != null ? ((Number) rhs).shortValue() : (short) 0; + } + if (c == Float.class || c == float.class) + { + if (rhs instanceof String) + { + rhs = removeLeadingAndTrailingQuotes((String) rhs); + if ("".equals(rhs)) + { + rhs = "0.0f"; + } + return Float.parseFloat((String)rhs); + } + return rhs != null ? ((Number) rhs).floatValue() : 0.0f; + } + + return error("Class '" + c.getName() + "' requested for special instantiation - isPrimitive() does not match newPrimitiveWrapper()"); + } + + static String removeLeadingAndTrailingQuotes(String s) + { + Matcher m = _extraQuotes.matcher(s); + if (m.find()) + { + s = m.group(2); + } + return s; + } + + private static boolean isDigit(int c) + { + return c >= '0' && c <= '9'; + } + + private static boolean isWhitespace(int c) + { + return c == ' ' || c == '\t' || c == '\n' || c == '\r'; + } + + private static Class classForName(String name) throws IOException + { + if (name == null || name.isEmpty()) + { + error("Invalid class name specified"); + } + try + { + Class c = _nameToClass.get(name); + return c == null ? loadClass(name) : c; + } + catch (ClassNotFoundException e) + { + return (Class) error("Class instance '" + name + "' could not be created", e); + } + } + + static Class classForName2(String name) throws IOException + { + if (name == null || name.isEmpty()) + { + error("Empty class name."); + } + try + { + Class c = _nameToClass.get(name); + return c == null ? loadClass(name) : c; + } + catch (ClassNotFoundException e) + { + error("Class instance '" + name + "' could not be created.", e); + return null; + } + } + + // loadClass() provided by: Thomas Margreiter + private static Class loadClass(String name) throws ClassNotFoundException + { + String className = name; + boolean arrayType = false; + Class primitiveArray = null; + + while (className.startsWith("[")) + { + arrayType = true; + if (className.endsWith(";")) className = className.substring(0,className.length()-1); + if (className.equals("[B")) primitiveArray = byte[].class; + else if (className.equals("[S")) primitiveArray = short[].class; + else if (className.equals("[I")) primitiveArray = int[].class; + else if (className.equals("[J")) primitiveArray = long[].class; + else if (className.equals("[F")) primitiveArray = float[].class; + else if (className.equals("[D")) primitiveArray = double[].class; + else if (className.equals("[Z")) primitiveArray = boolean[].class; + else if (className.equals("[C")) primitiveArray = char[].class; + int startpos = className.startsWith("[L") ? 2 : 1; + className = className.substring(startpos); + } + Class currentClass = null; + if (null == primitiveArray) + { + currentClass = Thread.currentThread().getContextClassLoader().loadClass(className); + } + + if (arrayType) + { + currentClass = (null != primitiveArray) ? primitiveArray : Array.newInstance(currentClass, 0).getClass(); + while (name.startsWith("[[")) + { + currentClass = Array.newInstance(currentClass, 0).getClass(); + name = name.substring(1); + } + } + return currentClass; + } + + /** + * Get a Field object using a String field name and a Class instance. This + * method will start on the Class passed in, and if not found there, will + * walk up super classes until it finds the field, or throws an IOException + * if it cannot find the field. + * + * @param c Class containing the desired field. + * @param fieldName String name of the desired field. + * @return Field object obtained from the passed in class (by name). The Field + * returned is cached so that it is only obtained via reflection once. + * @throws IOException for stream errors or parsing errors. + */ + private static Field getDeclaredField(Class c, String fieldName) throws IOException + { + return JsonWriter.getDeepDeclaredFields(c).get(fieldName); + } + + /** + * Read until non-whitespace character and then return it. + * This saves extra read/pushback. + * + * @return int representing the next non-whitespace character in the stream. + * @throws IOException for stream errors or parsing errors. + */ + private int skipWhitespaceRead() throws IOException + { + final FastPushbackReader in = _in; + int c = in.read(); + while (isWhitespace(c)) + { + c = in.read(); + } + + return c; + } + + private void skipWhitespace() throws IOException + { + _in.unread(skipWhitespaceRead()); + } + + public void close() + { + try + { + if (_in != null) + { + _in.close(); + } + } + catch (IOException ignored) { } + } + + /** + * For all fields where the value was "@ref":"n" where 'n' was the id of an object + * that had not yet been encountered in the stream, make the final substitution. + * @throws IOException + */ + private void patchUnresolvedReferences() throws IOException + { + Iterator i = _unresolvedRefs.iterator(); + while (i.hasNext()) + { + UnresolvedReference ref = (UnresolvedReference) i.next(); + Object objToFix = ref.referencingObj.target; + JsonObject objReferenced = _objsRead.get(ref.refId); + + if (objReferenced == null) + { + // System.err.println("Back reference (" + ref.refId + ") does not match any object id in input, field '" + ref.field + '\''); + continue; + } + + if (objReferenced.target == null) + { + // System.err.println("Back referenced object does not exist, @ref " + ref.refId + ", field '" + ref.field + '\''); + continue; + } + + if (objToFix == null) + { + // System.err.println("Referencing object is null, back reference, @ref " + ref.refId + ", field '" + ref.field + '\''); + continue; + } + + if (ref.index >= 0) + { // Fix []'s and Collections containing a forward reference. + if (objToFix instanceof List) + { // Patch up Indexable Collections + List list = (List) objToFix; + list.set(ref.index, objReferenced.target); + } + else if (objToFix instanceof Collection) + { // Add element (since it was not indexable, add it to collection) + Collection col = (Collection) objToFix; + col.add(objReferenced.target); + } + else + { + Array.set(objToFix, ref.index, objReferenced.target); // patch array element here + } + } + else + { // Fix field forward reference + Field field = getDeclaredField(objToFix.getClass(), ref.field); + if (field != null) + { + try + { + field.set(objToFix, objReferenced.target); // patch field here + } + catch (Exception e) + { + error("Error setting field while resolving references '" + field.getName() + "', @ref = " + ref.refId, e); + } + } + } + + i.remove(); + } + + int count = _unresolvedRefs.size(); + if (count > 0) + { + StringBuilder out = new StringBuilder(); + out.append(count); + out.append(" unresolved references:\n"); + i = _unresolvedRefs.iterator(); + count = 1; + + while (i.hasNext()) + { + UnresolvedReference ref = (UnresolvedReference) i.next(); + out.append(" Unresolved reference "); + out.append(count); + out.append('\n'); + out.append(" @ref "); + out.append(ref.refId); + out.append('\n'); + out.append(" field "); + out.append(ref.field); + out.append("\n\n"); + count++; + } + error(out.toString()); + } + } + + /** + * Process Maps/Sets (fix up their internal indexing structure) + * This is required because Maps hash items using hashCode(), which will + * change between VMs. Rehashing the map fixes this. + * + * If _noObjects==true, then move @keys to keys and @items to values + * and then drop these two entries from the map. + */ + private void rehashMaps() + { + final boolean useMaps = _noObjects; + for (Object[] mapPieces : _prettyMaps) + { + JsonObject jObj = (JsonObject) mapPieces[0]; + Object[] javaKeys, javaValues; + Map map; + + if (useMaps) + { // Make the @keys be the actual keys of the map. + map = jObj; + javaKeys = (Object[]) jObj.remove("@keys"); + javaValues = (Object[]) jObj.remove("@items"); + } + else + { + map = (Map) jObj.target; + javaKeys = (Object[]) mapPieces[1]; + javaValues = (Object[]) mapPieces[2]; + jObj.clear(); + } + + int j=0; + + while (javaKeys != null && j < javaKeys.length) + { + map.put(javaKeys[j], javaValues[j]); + j++; + } + } + } + + private static String getErrorMessage(String msg) + { + return msg + "\nLast read: " + getLastReadSnippet() + "\nline: " + _line.get() + ", col: " + _col.get(); + } + + static Object error(String msg) throws IOException + { + throw new IOException(getErrorMessage(msg)); + } + + static Object error(String msg, Exception e) throws IOException + { + throw new IOException(getErrorMessage(msg), e); + } + + private static String getLastReadSnippet() + { + StringBuilder s = new StringBuilder(); + for (char[] chars : _snippet.get()) + { + s.append(chars); + } + return s.toString(); + } + + /** + * This is a performance optimization. The lowest 128 characters are re-used. + * + * @param c char to match to a Character. + * @return a Character that matches the passed in char. If the value is + * less than 127, then the same Character instances are re-used. + */ + private static Character valueOf(char c) + { + return c <= 127 ? _charCache[(int) c] : c; + } + + public static final int MAX_CODE_POINT = 0x10ffff; + public static final int MIN_SUPPLEMENTARY_CODE_POINT = 0x010000; + public static final char MIN_LOW_SURROGATE = '\uDC00'; + public static final char MIN_HIGH_SURROGATE = '\uD800'; + + private static char[] toChars(int codePoint) + { + if (codePoint < 0 || codePoint > MAX_CODE_POINT) + { // int UTF-8 char must be in range + throw new IllegalArgumentException("value ' + codePoint + ' outside UTF-8 range"); + } + + if (codePoint < MIN_SUPPLEMENTARY_CODE_POINT) + { // if the int character fits in two bytes... + return new char[]{(char) codePoint}; + } + + char[] result = new char[2]; + int offset = codePoint - MIN_SUPPLEMENTARY_CODE_POINT; + result[1] = (char) ((offset & 0x3ff) + MIN_LOW_SURROGATE); + result[0] = (char) ((offset >>> 10) + MIN_HIGH_SURROGATE); + return result; + } + + /** + * This class adds significant performance increase over using the JDK + * PushbackReader. This is due to this class not using synchronization + * as it is not needed. + */ + private static class FastPushbackReader extends FilterReader + { + private final int[] _buf; + private int _idx; + + private FastPushbackReader(Reader reader, int size) + { + super(reader); + _snippet.get().clear(); + _line.set(1); + _col.set(1); + if (size <= 0) + { + throw new IllegalArgumentException("size <= 0"); + } + _buf = new int[size]; + _idx = size; + } + + private FastPushbackReader(Reader r) + { + this(r, 1); + } + + public int read() throws IOException + { + int ch; + + if (_idx < _buf.length) + { // read from push-back buffer + ch = _buf[_idx++]; + } + else + { + ch = super.read(); + } + if (ch >= 0) + { + if (ch == 0x0a) + { + _line.set(_line.get() + 1); + _col.set(0); + } + else + { + _col.set(_col.get() + 1); + } + Deque buffer = _snippet.get(); + buffer.addLast(toChars(ch)); + if (buffer.size() > 100) + { + buffer.removeFirst(); + } + } + return ch; + } + + public void unread(int c) throws IOException + { + if (_idx == 0) + { + error("unread(int c) called more than buffer size (" + _buf.length + ")"); + } + if (c == 0x0a) + { + _line.set(_line.get() - 1); + } + else + { + _col.set(_col.get() - 1); + } + _buf[--_idx] = c; + _snippet.get().removeLast(); + } + + /** + * Closes the stream and releases any system resources associated with + * it. Once the stream has been closed, further read(), + * unread(), ready(), or skip() invocations will throw an IOException. + * Closing a previously closed stream has no effect. + * + * @throws java.io.IOException If an I/O error occurs + */ + public void close() throws IOException + { + super.close(); + _snippet.remove(); + _line.remove(); + _col.remove(); + } + } +} diff --git a/src/main/cedarsoftware/util/io/JsonWriter.java b/src/main/cedarsoftware/util/io/JsonWriter.java new file mode 100644 index 0000000..78bc126 --- /dev/null +++ b/src/main/cedarsoftware/util/io/JsonWriter.java @@ -0,0 +1,2061 @@ +package cedarsoftware.util.io; + +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.Flushable; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.Timestamp; +import java.text.Format; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Output a Java object graph in JSON format. This code handles cyclic + * references and can serialize any Object graph without requiring a class + * to be 'Serializeable' or have any specific methods on it. + *

  • + * Call the static method: {@code JsonWriter.toJson(employee)}. This will + * convert the passed in 'employee' instance into a JSON String.
  • + *
  • Using streams: + *
         JsonWriter writer = new JsonWriter(stream);
    + *     writer.write(employee);
    + *     writer.close();
    + * This will write the 'employee' object to the passed in OutputStream. + *
  • + *

    That's it. This can be used as a debugging tool. Output an object + * graph using the above code. You can copy that JSON output into this site + * which formats it with a lot of whitespace to make it human readable: + * http://jsonformatter.curiousconcept.com + *

    + *

    This will output any object graph deeply (or null). Object references are + * properly handled. For example, if you had A->B, B->C, and C->A, then + * A will be serialized with a B object in it, B will be serialized with a C + * object in it, and then C will be serialized with a reference to A (ref), not a + * redefinition of A.

    + *
    + * + * @author John DeRegnaucourt (jdereg@gmail.com) + *
    + * Copyright (c) Cedar Software LLC + *

    + * 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. + */ +public class JsonWriter implements Closeable, Flushable +{ + public static final String DATE_FORMAT = "DATE_FORMAT"; + public static final String ISO_DATE_FORMAT = "yyyy-MM-dd"; + public static final String ISO_DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"; + public static final String TYPE = "TYPE"; + public static final String PRETTY_PRINT = "PRETTY_PRINT"; + public static final String FIELD_SPECIFIERS = "FIELD_SPECIFIERS"; + private static final Map _classMetaCache = new ConcurrentHashMap(); + private static final List _writers = new ArrayList(); + private static final Set _notCustom = new HashSet(); + private static Object[] _byteStrings = new Object[256]; + private static final String newLine = System.getProperty("line.separator"); + private final Map _objVisited = new IdentityHashMap(); + private final Map _objsReferenced = new IdentityHashMap(); + private final Writer _out; + private long _identity = 1; + private int depth = 0; + // _args is using ThreadLocal so that static inner classes can have access to them + static final ThreadLocal> _args = new ThreadLocal>() + { + public Map initialValue() + { + return new HashMap(); + } + }; + static final ThreadLocal _dateFormat = new ThreadLocal() + { + public SimpleDateFormat initialValue() + { + return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + } + }; + + static + { // Add customer writers (these make common classes more succinct) + addWriter(String.class, new JsonStringWriter()); + addWriter(Date.class, new DateWriter()); + addWriter(BigInteger.class, new BigIntegerWriter()); + addWriter(BigDecimal.class, new BigDecimalWriter()); + addWriter(java.sql.Date.class, new DateWriter()); + addWriter(Timestamp.class, new TimestampWriter()); + addWriter(Calendar.class, new CalendarWriter()); + addWriter(TimeZone.class, new TimeZoneWriter()); + addWriter(Locale.class, new LocaleWriter()); + addWriter(Class.class, new ClassWriter()); + addWriter(StringBuilder.class, new StringBuilderWriter()); + addWriter(StringBuffer.class, new StringBufferWriter()); + } + + static + { + for (short i = -128; i <= 127; i++) + { + char[] chars = Integer.toString(i).toCharArray(); + _byteStrings[i + 128] = chars; + } + } + + static class ClassMeta extends LinkedHashMap + { + } + + @Deprecated + public static String toJson(Object item) + { + throw new RuntimeException("Use com.cedarsoftware.util.JsonWriter.objectToJson()"); + } + + @Deprecated + public static String toJson(Object item, Map optionalArgs) + { + throw new RuntimeException("Use com.cedarsoftware.util.JsonWriter.objectToJson()"); + } + + /** + * @see JsonWriter#objectToJson(Object, java.util.Map) + */ + public static String objectToJson(Object item) throws IOException + { + return objectToJson(item, new HashMap()); + } + + /** + * Convert a Java Object to a JSON String. + * + * @param item Object to convert to a JSON String. + * @param optionalArgs (optional) Map of extra arguments indicating how dates are formatted, + * what fields are written out (optional). For Date parameters, use the public static + * DATE_TIME key, and then use the ISO_DATE or ISO_DATE_TIME indicators. Or you can specify + * your own custom SimpleDateFormat String, or you can associate a SimpleDateFormat object, + * in which case it will be used. This setting is for both java.util.Date and java.sql.Date. + * If the DATE_FORMAT key is not used, then dates will be formatted as longs. This long can + * be turned back into a date by using 'new Date(longValue)'. + * @return String containing JSON representation of passed + * in object. + * @throws java.io.IOException If an I/O error occurs + */ + public static String objectToJson(Object item, Map optionalArgs) throws IOException + { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + JsonWriter writer = new JsonWriter(stream, optionalArgs); + writer.write(item); + writer.close(); + return new String(stream.toByteArray(), "UTF-8"); + } + + /** + * Format the passed in JSON string in a nice, human readable format. + * @param json String input JSON + * @return String containing equivalent JSON, formatted nicely for human readability. + */ + public static String formatJson(String json) throws IOException + { + Map map = JsonReader.jsonToMaps(json); + Map args = new HashMap(); + args.put(PRETTY_PRINT, "true"); + return objectToJson(map, args); + } + + /** + * @see JsonWriter#JsonWriter(java.io.OutputStream, java.util.Map) + */ + public JsonWriter(OutputStream out) throws IOException + { + this(out, new HashMap()); + } + + /** + * @param out OutputStream to which the JSON output will be written. + * @param optionalArgs (optional) Map of extra arguments indicating how dates are formatted, + * what fields are written out (optional). For Date parameters, use the public static + * DATE_TIME key, and then use the ISO_DATE or ISO_DATE_TIME indicators. Or you can specify + * your own custom SimpleDateFormat String, or you can associate a SimpleDateFormat object, + * in which case it will be used. This setting is for both java.util.Date and java.sql.Date. + * If the DATE_FORMAT key is not used, then dates will be formatted as longs. This long can + * be turned back into a date by using 'new Date(longValue)'. + * @throws IOException + */ + public JsonWriter(OutputStream out, Map optionalArgs) throws IOException + { + Map args = _args.get(); + args.clear(); + args.putAll(optionalArgs); + + if (!optionalArgs.containsKey(FIELD_SPECIFIERS)) + { // Ensure that at least an empty Map is in the FIELD_SPECIFIERS entry + args.put(FIELD_SPECIFIERS, new HashMap()); + } + else + { // Convert String field names to Java Field instances (makes it easier for user to set this up) + Map> specifiers = (Map>) args.get(FIELD_SPECIFIERS); + Map> copy = new HashMap>(); + for (Map.Entry> entry : specifiers.entrySet()) + { + Class c = entry.getKey(); + List fields = entry.getValue(); + List newList = new ArrayList(fields.size()); + + ClassMeta meta = getDeepDeclaredFields(c); + + for (String field : fields) + { + Field f = meta.get(field); + if (f == null) + { + throw new IllegalArgumentException("Unable to locate field: " + field + " on class: " + c.getName() + ". Make sure the fields in the FIELD_SPECIFIERS map existing on the associated class."); + } + newList.add(f); + } + copy.put(c, newList); + } + args.put(FIELD_SPECIFIERS, copy); + } + + try + { + _out = new BufferedWriter(new OutputStreamWriter(out, "UTF-8")); + } + catch (UnsupportedEncodingException e) + { + throw new IOException("Unsupported encoding. Get a JVM that supports UTF-8", e); + } + } + + public interface JsonClassWriter + { + void write(Object o, boolean showType, Writer out) throws IOException; + boolean hasPrimitiveForm(); + void writePrimitiveForm(Object o, Writer out) throws IOException; + } + + public boolean isPrettyPrint() + { + Object setting = _args.get().get(PRETTY_PRINT); + if (setting instanceof Boolean) + { + return Boolean.TRUE.equals(setting); + } + else if (setting instanceof String) + { + return "true".equalsIgnoreCase((String) setting); + } + else if (setting instanceof Number) + { + return ((Number)setting).intValue() != 0; + } + + return false; + } + + private void tabIn(Writer out) throws IOException + { + if (!isPrettyPrint()) + { + return; + } + out.write(newLine); + depth++; + for (int i=0; i < depth; i++) + { + out.write(" "); + } + } + + private void newLine(Writer out) throws IOException + { + if (!isPrettyPrint()) + { + return; + } + out.write(newLine); + for (int i=0; i < depth; i++) + { + out.write(" "); + } + } + + private void tabOut(Writer out) throws IOException + { + if (!isPrettyPrint()) + { + return; + } + out.write(newLine); + depth--; + for (int i=0; i < depth; i++) + { + out.write(" "); + } + } + + public static int getDistance(Class a, Class b) + { + Class curr = b; + int distance = 0; + + while (curr != a) + { + distance++; + curr = curr.getSuperclass(); + if (curr == null) + { + return Integer.MAX_VALUE; + } + } + + return distance; + } + + public boolean writeIfMatching(Object o, boolean showType, Writer out) throws IOException + { + Class c = o.getClass(); + if (_notCustom.contains(c)) + { + return false; + } + + return writeCustom(c, o, showType, out); + } + + public boolean writeArrayElementIfMatching(Class arrayComponentClass, Object o, boolean showType, Writer out) throws IOException + { + if (!o.getClass().isAssignableFrom(arrayComponentClass) || _notCustom.contains(o.getClass())) + { + return false; + } + + return writeCustom(arrayComponentClass, o, showType, out); + } + + private boolean writeCustom(Class arrayComponentClass, Object o, boolean showType, Writer out) throws IOException + { + JsonClassWriter closestWriter = null; + int minDistance = Integer.MAX_VALUE; + + for (Object[] item : _writers) + { + Class clz = (Class)item[0]; + if (clz == arrayComponentClass) + { + closestWriter = (JsonClassWriter)item[1]; + break; + } + int distance = getDistance(clz, arrayComponentClass); + if (distance < minDistance) + { + minDistance = distance; + closestWriter = (JsonClassWriter)item[1]; + } + } + + if (closestWriter == null) + { + return false; + } + + if (writeOptionalReference(o)) + { + return true; + } + + boolean referenced = _objsReferenced.containsKey(o); + + if ((!referenced && !showType && closestWriter.hasPrimitiveForm()) || closestWriter instanceof JsonStringWriter) + { + closestWriter.writePrimitiveForm(o, out); + return true; + } + + out.write('{'); + tabIn(out); + if (referenced) + { + writeId(getId(o)); + if (showType) + { + out.write(','); + newLine(out); + } + } + + if (showType) + { + writeType(o, out); + } + + if (referenced || showType) + { + out.write(','); + newLine(out); + } + + closestWriter.write(o, showType || referenced, out); + tabOut(out); + out.write('}'); + return true; + } + + public static void addWriter(Class c, JsonClassWriter writer) + { + for (Object[] item : _writers) + { + Class clz = (Class)item[0]; + if (clz == c) + { + item[1] = writer; // Replace writer + return; + } + } + _writers.add(new Object[] {c, writer}); + } + + public static void addNotCustomWriter(Class c) + { + _notCustom.add(c); + } + + public static class TimeZoneWriter implements JsonClassWriter + { + public void write(Object obj, boolean showType, Writer out) throws IOException + { + TimeZone cal = (TimeZone) obj; + out.write("\"zone\":\""); + out.write(cal.getID()); + out.write('"'); + } + + public boolean hasPrimitiveForm() { return false; } + public void writePrimitiveForm(Object o, Writer out) throws IOException {} + } + + public static class CalendarWriter implements JsonClassWriter + { + public void write(Object obj, boolean showType, Writer out) throws IOException + { + Calendar cal = (Calendar) obj; + _dateFormat.get().setTimeZone(cal.getTimeZone()); + out.write("\"time\":\""); + out.write(_dateFormat.get().format(cal.getTime())); + out.write("\",\"zone\":\""); + out.write(cal.getTimeZone().getID()); + out.write('"'); + } + + public boolean hasPrimitiveForm() { return false; } + public void writePrimitiveForm(Object o, Writer out) throws IOException {} + } + + public static class DateWriter implements JsonClassWriter + { + public void write(Object obj, boolean showType, Writer out) throws IOException + { + Date date = (Date)obj; + Object dateFormat = _args.get().get(DATE_FORMAT); + if (dateFormat instanceof String) + { // Passed in as String, turn into a SimpleDateFormat instance to be used throughout this stream write. + dateFormat = new SimpleDateFormat((String) dateFormat, Locale.ENGLISH); + _args.get().put(DATE_FORMAT, dateFormat); + } + if (showType) + { + out.write("\"value\":"); + } + + if (dateFormat instanceof Format) + { + out.write("\""); + out.write(((Format)dateFormat).format(date)); + out.write("\""); + } + else + { + out.write(Long.toString(((Date) obj).getTime())); + } + } + + public boolean hasPrimitiveForm() { return true; } + + public void writePrimitiveForm(Object o, Writer out) throws IOException + { + if (_args.get().containsKey(DATE_FORMAT)) + { + write(o, false, out); + } + else + { + out.write(Long.toString(((Date) o).getTime())); + } + } + } + + public static class TimestampWriter implements JsonClassWriter + { + public void write(Object o, boolean showType, Writer out) throws IOException + { + Timestamp tstamp = (Timestamp) o; + out.write("\"time\":\""); + out.write(Long.toString((tstamp.getTime() / 1000) * 1000)); + out.write("\",\"nanos\":\""); + out.write(Integer.toString(tstamp.getNanos())); + out.write('"'); + } + + public boolean hasPrimitiveForm() { return false; } + + public void writePrimitiveForm(Object o, Writer out) throws IOException { } + } + + public static class ClassWriter implements JsonClassWriter + { + public void write(Object obj, boolean showType, Writer out) throws IOException + { + String value = ((Class) obj).getName(); + out.write("\"value\":"); + writeJsonUtf8String(value, out); + } + + public boolean hasPrimitiveForm() { return true; } + + public void writePrimitiveForm(Object o, Writer out) throws IOException + { + writeJsonUtf8String(((Class)o).getName(), out); + } + } + + public static class JsonStringWriter implements JsonClassWriter + { + public void write(Object obj, boolean showType, Writer out) throws IOException + { + out.write("\"value\":"); + writeJsonUtf8String((String) obj, out); + } + + public boolean hasPrimitiveForm() { return true; } + + public void writePrimitiveForm(Object o, Writer out) throws IOException + { + writeJsonUtf8String((String) o, out); + } + } + + public static class LocaleWriter implements JsonClassWriter + { + public void write(Object obj, boolean showType, Writer out) throws IOException + { + Locale locale = (Locale) obj; + + out.write("\"language\":\""); + out.write(locale.getLanguage()); + out.write("\",\"country\":\""); + out.write(locale.getCountry()); + out.write("\",\"variant\":\""); + out.write(locale.getVariant()); + out.write('"'); + } + public boolean hasPrimitiveForm() { return false; } + public void writePrimitiveForm(Object o, Writer out) throws IOException { } + } + + public static class BigIntegerWriter implements JsonClassWriter + { + public void write(Object obj, boolean showType, Writer out) throws IOException + { + BigInteger big = (BigInteger) obj; + out.write("\"value\":\""); + out.write(big.toString(10)); + out.write('"'); + } + + public boolean hasPrimitiveForm() { return true; } + + public void writePrimitiveForm(Object o, Writer out) throws IOException + { + BigInteger big = (BigInteger) o; + out.write('"'); + out.write(big.toString(10)); + out.write('"'); + } + } + + public static class BigDecimalWriter implements JsonClassWriter + { + public void write(Object obj, boolean showType, Writer out) throws IOException + { + BigDecimal big = (BigDecimal) obj; + out.write("\"value\":\""); + out.write(big.toPlainString()); + out.write('"'); + } + + public boolean hasPrimitiveForm() { return true; } + + public void writePrimitiveForm(Object o, Writer out) throws IOException + { + BigDecimal big = (BigDecimal) o; + out.write('"'); + out.write(big.toPlainString()); + out.write('"'); + } + } + + public static class StringBuilderWriter implements JsonClassWriter + { + public void write(Object obj, boolean showType, Writer out) throws IOException + { + StringBuilder builder = (StringBuilder) obj; + out.write("\"value\":\""); + out.write(builder.toString()); + out.write('"'); + } + + public boolean hasPrimitiveForm() { return true; } + + public void writePrimitiveForm(Object o, Writer out) throws IOException + { + StringBuilder builder = (StringBuilder) o; + out.write('"'); + out.write(builder.toString()); + out.write('"'); + } + } + + public static class StringBufferWriter implements JsonClassWriter + { + public void write(Object obj, boolean showType, Writer out) throws IOException + { + StringBuffer buffer = (StringBuffer) obj; + out.write("\"value\":\""); + out.write(buffer.toString()); + out.write('"'); + } + + public boolean hasPrimitiveForm() { return true; } + + public void writePrimitiveForm(Object o, Writer out) throws IOException + { + StringBuffer buffer = (StringBuffer) o; + out.write('"'); + out.write(buffer.toString()); + out.write('"'); + } + } + + public void write(Object obj) throws IOException + { + traceReferences(obj); + _objVisited.clear(); + writeImpl(obj, true); + flush(); + _objVisited.clear(); + _objsReferenced.clear(); + _args.get().clear(); + _args.remove(); + } + + private void traceReferences(Object root) + { + LinkedList stack = new LinkedList(); + stack.addFirst(root); + final Map visited = _objVisited; + final Map referenced = _objsReferenced; + + while (!stack.isEmpty()) + { + Object obj = stack.removeFirst(); + if (obj == null) + { + continue; + } + + if (!JsonReader.isPrimitive(obj.getClass()) && !(obj instanceof String) && !(obj instanceof Date)) + { + Long id = visited.get(obj); + if (id != null) + { // Only write an object once. + referenced.put(obj, id); + continue; + } + visited.put(obj, _identity++); + } + + final Class clazz = obj.getClass(); + + if (clazz.isArray()) + { + Class compType = clazz.getComponentType(); + if (!JsonReader.isPrimitive(compType) && compType != String.class && !Date.class.isAssignableFrom(compType)) + { // Speed up: do not traceReferences of primitives, they cannot reference anything + final int len = Array.getLength(obj); + + for (int i = 0; i < len; i++) + { + Object o = Array.get(obj, i); + if (o != null) + { // Slight perf gain (null is legal) + stack.addFirst(o); + } + } + } + } + else + { + traceFields(stack, obj); + } + } + } + + /** + * Reach-ability trace to visit all objects within the graph to be written. + * This API will handle any object, using either reflection APIs or by + * consulting a specified FIELD_SPECIFIERS map if provided. + */ + private static void traceFields(LinkedList stack, Object obj) + { + final ClassMeta fields = getDeepDeclaredFields(obj.getClass()); + Map> fieldSpecifiers = (Map) _args.get().get(FIELD_SPECIFIERS); + + // If caller has special Field specifier for a given class + // then use it, otherwise use reflection. + List fieldSet = getFieldsUsingSpecifier(obj.getClass(), fieldSpecifiers); + if (fieldSet != null) + { // Trace fields using external field specifier (explicitly tells us which fields to use for a given class) + for (Field field : fieldSet) + { + traceField(stack, obj, field); + } + } + else + { // Trace fields using reflection + for (Field field : fields.values()) + { + traceField(stack, obj, field); + } + } + } + + /** + * Push object associated to field onto stack for further tracing. If object was a primitive, + * Date, String, or null, no further tracing is done. + */ + private static void traceField(LinkedList stack, Object obj, Field field) + { + try + { + final Class type = field.getType(); + if (JsonReader.isPrimitive(type) || String.class == type || Date.class.isAssignableFrom(type)) + { // speed up: primitives (Dates/Strings considered primitive by json-io) cannot reference another object + return; + } + + Object o = field.get(obj); + if (o != null) + { + stack.addFirst(o); + } + } + catch (Exception ignored) { } + } + + private static List getFieldsUsingSpecifier(Class classBeingWritten, Map> fieldSpecifiers) + { + Iterator>> i = fieldSpecifiers.entrySet().iterator(); + int minDistance = Integer.MAX_VALUE; + List fields = null; + + while (i.hasNext()) + { + Map.Entry> entry = i.next(); + Class c = entry.getKey(); + if (c == classBeingWritten) + { + return entry.getValue(); + } + + int distance = getDistance(c, classBeingWritten); + + if (distance < minDistance) + { + minDistance = distance; + fields = entry.getValue(); + } + } + + return fields; + } + + private boolean writeOptionalReference(Object obj) throws IOException + { + final Writer out = _out; + if (_objVisited.containsKey(obj)) + { // Only write (define) an object once in the JSON stream, otherwise emit a @ref + String id = getId(obj); + if (id == null) + { // Test for null because of Weak/Soft references being gc'd during serialization. + return false; + } + out.write("{\"@ref\":"); + out.write(id); + out.write('}'); + return true; + } + + // Mark the object as visited by putting it in the Map (this map is re-used / clear()'d after walk()). + _objVisited.put(obj, null); + return false; + } + + private void writeImpl(Object obj, boolean showType) throws IOException + { + if (obj == null) + { + _out.write("null"); + return; + } + + if (obj.getClass().isArray()) + { + writeArray(obj, showType); + } + else if (obj instanceof Collection) + { + writeCollection((Collection) obj, showType); + } + else if (obj instanceof JsonObject) + { // symmetric support for writing Map of Maps representation back as identical JSON format. + JsonObject jObj = (JsonObject) obj; + if (jObj.isArray()) + { + writeJsonObjectArray(jObj, showType); + } + else if (jObj.isCollection()) + { + writeJsonObjectCollection(jObj, showType); + } + else if (jObj.isMap()) + { + writeJsonObjectMap(jObj, showType); + } + else + { + writeJsonObjectObject(jObj, showType); + } + } + else if (obj instanceof Map) + { + writeMap((Map) obj, showType); + } + else + { + writeObject(obj, showType); + } + } + + private void writeId(String id) throws IOException + { + _out.write("\"@id\":"); + _out.write(id == null ? "0" : id); + } + + private static void writeType(Object obj, Writer out) throws IOException + { + out.write("\"@type\":\""); + Class c = obj.getClass(); + + if (Boolean.class == c) + { + out.write("boolean"); + } + else if (Byte.class == c) + { + out.write("byte"); + } + else if (Short.class == c) + { + out.write("short"); + } + else if (Integer.class == c) + { + out.write("int"); + } + else if (Long.class == c) + { + out.write("long"); + } + else if (Double.class == c) + { + out.write("double"); + } + else if (Float.class == c) + { + out.write("float"); + } + else if (Character.class == c) + { + out.write("char"); + } + else if (Date.class == c) + { + out.write("date"); + } + else if (Class.class == c) + { + out.write("class"); + } + else if (String.class == c) + { + out.write("string"); + } + else + { + out.write(c.getName()); + } + out.write('"'); + } + + private void writePrimitive(Object obj) throws IOException + { + if (obj instanceof Character) + { + writeJsonUtf8String(String.valueOf(obj), _out); + } + else + { + _out.write(obj.toString()); + } + } + + private void writeArray(Object array, boolean showType) throws IOException + { + if (writeOptionalReference(array)) + { + return; + } + + Class arrayType = array.getClass(); + int len = Array.getLength(array); + boolean referenced = _objsReferenced.containsKey(array); +// boolean typeWritten = showType && !(Object[].class == arrayType); // causes IDE warning in NetBeans 7/4 Java 1.7 + boolean typeWritten = showType && !(arrayType.equals(Object[].class)); + + final Writer out = _out; // performance opt: place in final local for quicker access + if (typeWritten || referenced) + { + out.write('{'); + tabIn(out); + } + + if (referenced) + { + writeId(getId(array)); + out.write(','); + newLine(out); + } + + if (typeWritten) + { + writeType(array, out); + out.write(','); + newLine(out); + } + + if (len == 0) + { + if (typeWritten || referenced) + { + out.write("\"@items\":[]"); + tabOut(out); + out.write('}'); + } + else + { + out.write("[]"); + } + return; + } + + if (typeWritten || referenced) + { + out.write("\"@items\":["); + } + else + { + out.write('['); + } + tabIn(out); + + final int lenMinus1 = len - 1; + + // Intentionally processing each primitive array type in separate + // custom loop for speed. All of them could be handled using + // reflective Array.get() but it is slower. I chose speed over code length. + if (byte[].class == arrayType) + { + writeByteArray((byte[]) array, lenMinus1); + } + else if (char[].class == arrayType) + { + writeJsonUtf8String(new String((char[]) array), out); + } + else if (short[].class == arrayType) + { + writeShortArray((short[]) array, lenMinus1); + } + else if (int[].class == arrayType) + { + writeIntArray((int[]) array, lenMinus1); + } + else if (long[].class == arrayType) + { + writeLongArray((long[]) array, lenMinus1); + } + else if (float[].class == arrayType) + { + writeFloatArray((float[]) array, lenMinus1); + } + else if (double[].class == arrayType) + { + writeDoubleArray((double[]) array, lenMinus1); + } + else if (boolean[].class == arrayType) + { + writeBooleanArray((boolean[]) array, lenMinus1); + } + else + { + final Class componentClass = array.getClass().getComponentType(); + final boolean isPrimitiveArray = JsonReader.isPrimitive(componentClass); + final boolean isObjectArray = Object[].class == arrayType; + + for (int i = 0; i < len; i++) + { + final Object value = Array.get(array, i); + + if (value == null) + { + out.write("null"); + } + else if (isPrimitiveArray || value instanceof Boolean || value instanceof Long || value instanceof Double) + { + writePrimitive(value); + } + else if (writeArrayElementIfMatching(componentClass, value, false, out)) { } + else if (isObjectArray) + { + if (writeIfMatching(value, true, out)) { } + else + { + writeImpl(value, true); + } + } + else + { // Specific Class-type arrays - only force type when + // the instance is derived from array base class. + boolean forceType = !(value.getClass() == componentClass); + writeImpl(value, forceType || alwaysShowType()); + } + + if (i != lenMinus1) + { + out.write(','); + newLine(out); + } + } + } + + tabOut(out); + out.write(']'); + if (typeWritten || referenced) + { + tabOut(out); + out.write('}'); + } + } + + /** + * @return true if the user set the 'TYPE' flag to true, indicating to always show type. + */ + private boolean alwaysShowType() + { + return Boolean.TRUE.equals(_args.get().get(TYPE)); + } + + private void writeBooleanArray(boolean[] booleans, int lenMinus1) throws IOException + { + final Writer out = _out; + for (int i = 0; i < lenMinus1; i++) + { + out.write(booleans[i] ? "true," : "false,"); + } + out.write(Boolean.toString(booleans[lenMinus1])); + } + + private void writeDoubleArray(double[] doubles, int lenMinus1) throws IOException + { + final Writer out = _out; + for (int i = 0; i < lenMinus1; i++) + { + out.write(Double.toString(doubles[i])); + out.write(','); + } + out.write(Double.toString(doubles[lenMinus1])); + } + + private void writeFloatArray(float[] floats, int lenMinus1) throws IOException + { + final Writer out = _out; + for (int i = 0; i < lenMinus1; i++) + { + out.write(Double.toString(floats[i])); + out.write(','); + } + out.write(Float.toString(floats[lenMinus1])); + } + + private void writeLongArray(long[] longs, int lenMinus1) throws IOException + { + final Writer out = _out; + for (int i = 0; i < lenMinus1; i++) + { + out.write(Long.toString(longs[i])); + out.write(','); + } + out.write(Long.toString(longs[lenMinus1])); + } + + private void writeIntArray(int[] ints, int lenMinus1) throws IOException + { + final Writer out = _out; + for (int i = 0; i < lenMinus1; i++) + { + out.write(Integer.toString(ints[i])); + out.write(','); + } + out.write(Integer.toString(ints[lenMinus1])); + } + + private void writeShortArray(short[] shorts, int lenMinus1) throws IOException + { + final Writer out = _out; + for (int i = 0; i < lenMinus1; i++) + { + out.write(Integer.toString(shorts[i])); + out.write(','); + } + out.write(Integer.toString(shorts[lenMinus1])); + } + + private void writeByteArray(byte[] bytes, int lenMinus1) throws IOException + { + final Writer out = _out; + final Object[] byteStrs = _byteStrings; + for (int i = 0; i < lenMinus1; i++) + { + out.write((char[]) byteStrs[bytes[i] + 128]); + out.write(','); + } + out.write((char[]) byteStrs[bytes[lenMinus1] + 128]); + } + + private void writeCollection(Collection col, boolean showType) throws IOException + { + if (writeOptionalReference(col)) + { + return; + } + + final Writer out = _out; + boolean referenced = _objsReferenced.containsKey(col); + boolean isEmpty = col.isEmpty(); + + if (referenced || showType) + { + out.write('{'); + tabIn(out); + } + else if (isEmpty) + { + out.write('['); + } + + if (referenced) + { + writeId(getId(col)); + } + + if (showType) + { + if (referenced) + { + out.write(','); + newLine(out); + } + writeType(col, out); + } + + if (isEmpty) + { + if (referenced || showType) + { + tabOut(out); + out.write('}'); + } + else + { + out.write(']'); + } + return; + } + + if (showType || referenced) + { + out.write(','); + newLine(out); + out.write("\"@items\":["); + } + else + { + out.write('['); + } + tabIn(out); + + Iterator i = col.iterator(); + + while (i.hasNext()) + { + writeCollectionElement(i.next()); + + if (i.hasNext()) + { + out.write(','); + newLine(out); + } + + } + + tabOut(out); + out.write(']'); + if (showType || referenced) + { // Finished object, as it was output as an object if @id or @type was output + tabOut(out); + out.write("}"); + } + } + + private void writeJsonObjectArray(JsonObject jObj, boolean showType) throws IOException + { + if (writeOptionalReference(jObj)) + { + return; + } + + int len = jObj.getLength(); + String type = jObj.type; + Class arrayClass; + + if (type == null || Object[].class.getName().equals(type)) + { + arrayClass = Object[].class; + } + else + { + arrayClass = JsonReader.classForName2(type); + } + + final Writer out = _out; + final boolean isObjectArray = Object[].class == arrayClass; + final Class componentClass = arrayClass.getComponentType(); + boolean referenced = _objsReferenced.containsKey(jObj) && jObj.hasId(); + boolean typeWritten = showType && !isObjectArray; + + if (typeWritten || referenced) + { + out.write('{'); + tabIn(out); + } + + if (referenced) + { + writeId(Long.toString(jObj.id)); + out.write(','); + newLine(out); + } + + if (typeWritten) + { + out.write("\"@type\":\""); + out.write(arrayClass.getName()); + out.write("\","); + newLine(out); + } + + if (len == 0) + { + if (typeWritten || referenced) + { + out.write("\"@items\":[]"); + tabOut(out); + out.write("}"); + } + else + { + out.write("[]"); + } + return; + } + + if (typeWritten || referenced) + { + out.write("\"@items\":["); + } + else + { + out.write('['); + } + tabIn(out); + + Object[] items = (Object[]) jObj.get("@items"); + final int lenMinus1 = len - 1; + + for (int i = 0; i < len; i++) + { + final Object value = items[i]; + + if (value == null) + { + out.write("null"); + } + else if (Character.class == componentClass || char.class == componentClass) + { + writeJsonUtf8String((String) value, out); + } + else if (value instanceof Boolean || value instanceof Long || value instanceof Double) + { + writePrimitive(value); + } + else if (value instanceof String) + { // Have to specially treat String because it could be referenced, but we still want inline (no @type, value:) + writeJsonUtf8String((String) value, out); + } + else if (isObjectArray) + { + if (writeIfMatching(value, true, out)) { } + else + { + writeImpl(value, true); + } + } + else if (writeArrayElementIfMatching(componentClass, value, false, out)) { } + else + { // Specific Class-type arrays - only force type when + // the instance is derived from array base class. + boolean forceType = !(value.getClass() == componentClass); + writeImpl(value, forceType || alwaysShowType()); + } + + if (i != lenMinus1) + { + out.write(','); + newLine(out); + } + } + + tabOut(out); + out.write(']'); + if (typeWritten || referenced) + { + tabOut(out); + out.write('}'); + } + } + + private void writeJsonObjectCollection(JsonObject jObj, boolean showType) throws IOException + { + if (writeOptionalReference(jObj)) + { + return; + } + + String type = jObj.type; + Class colClass = JsonReader.classForName2(type); + boolean referenced = _objsReferenced.containsKey(jObj) && jObj.hasId(); + final Writer out = _out; + int len = jObj.getLength(); + + if (referenced || showType || len == 0) + { + out.write('{'); + tabIn(out); + } + + if (referenced) + { + writeId(String.valueOf(jObj.id)); + } + + if (showType) + { + if (referenced) + { + out.write(','); + newLine(out); + } + out.write("\"@type\":\""); + out.write(colClass.getName()); + out.write('"'); + } + + if (len == 0) + { + tabOut(out); + out.write('}'); + return; + } + + if (showType || referenced) + { + out.write(','); + newLine(out); + out.write("\"@items\":["); + } + else + { + out.write('['); + } + tabIn(out); + + Object[] items = (Object[]) jObj.get("@items"); + final int itemsLen = items.length; + final int itemsLenMinus1 = itemsLen - 1; + + for (int i=0; i < itemsLen; i++) + { + writeCollectionElement(items[i]); + + if (i != itemsLenMinus1) + { + out.write(','); + newLine(out); + } + } + + tabOut(out); + out.write("]"); + if (showType || referenced) + { + tabOut(out); + out.write('}'); + } + } + + private void writeJsonObjectMap(JsonObject jObj, boolean showType) throws IOException + { + if (writeOptionalReference(jObj)) + { + return; + } + + boolean referenced = _objsReferenced.containsKey(jObj) && jObj.hasId(); + final Writer out = _out; + + out.write('{'); + tabIn(out); + if (referenced) + { + writeId(String.valueOf(jObj.getId())); + } + + if (showType) + { + if (referenced) + { + out.write(','); + newLine(out); + } + String type = jObj.getType(); + if (type != null) + { + Class mapClass = JsonReader.classForName2(type); + out.write("\"@type\":\""); + out.write(mapClass.getName()); + out.write('"'); + } + else + { // type not displayed + showType = false; + } + } + + if (jObj.isEmpty()) + { // Empty + tabOut(out); + out.write('}'); + return; + } + + if (showType) + { + out.write(','); + newLine(out); + } + + out.write("\"@keys\":["); + tabIn(out); + Iterator i = jObj.keySet().iterator(); + + while (i.hasNext()) + { + writeCollectionElement(i.next()); + + if (i.hasNext()) + { + out.write(','); + newLine(out); + } + } + + tabOut(out); + out.write("],"); + newLine(out); + out.write("\"@items\":["); + tabIn(out); + i =jObj.values().iterator(); + + while (i.hasNext()) + { + writeCollectionElement(i.next()); + + if (i.hasNext()) + { + out.write(','); + newLine(out); + } + } + + tabOut(out); + out.write(']'); + tabOut(out); + out.write('}'); + } + + private void writeJsonObjectObject(JsonObject jObj, boolean showType) throws IOException + { + if (writeOptionalReference(jObj)) + { + return; + } + + final Writer out = _out; + boolean referenced = _objsReferenced.containsKey(jObj) && jObj.hasId(); + showType = showType && jObj.type != null; + Class type = null; + + out.write('{'); + tabIn(out); + if (referenced) + { + writeId(String.valueOf(jObj.id)); + } + + if (showType) + { + if (referenced) + { + out.write(','); + newLine(out); + } + out.write("\"@type\":\""); + out.write(jObj.type); + out.write('"'); + try { type = JsonReader.classForName2(jObj.type); } catch(Exception ignored) { type = null; } + } + + if (jObj.isEmpty()) + { + tabOut(out); + out.write('}'); + return; + } + + if (showType || referenced) + { + out.write(','); + newLine(out); + } + + Iterator> i = jObj.entrySet().iterator(); + + while (i.hasNext()) + { + Map.Entryentry = i.next(); + final String fieldName = entry.getKey(); + out.write('"'); + out.write(fieldName); + out.write("\":"); + Object value = entry.getValue(); + + if (value == null) + { + out.write("null"); + } + else if (value instanceof BigDecimal || value instanceof BigInteger) + { + writeImpl(value, !doesValueTypeMatchFieldType(type, fieldName, value)); + } + else if (value instanceof Number || value instanceof Boolean) + { + out.write(value.toString()); + } + else if (value instanceof String) + { + writeJsonUtf8String((String) value, out); + } + else if (value instanceof Character) + { + writeJsonUtf8String(String.valueOf(value), out); + } + else + { + writeImpl(value, !doesValueTypeMatchFieldType(type, fieldName, value)); + } + if (i.hasNext()) + { + out.write(','); + newLine(out); + } + } + tabOut(out); + out.write('}'); + } + + private static boolean doesValueTypeMatchFieldType(Class type, String fieldName, Object value) + { + if (type != null) + { + ClassMeta meta = getDeepDeclaredFields(type); + Field field = meta.get(fieldName); + return field != null && (value.getClass() == field.getType()); + } + return false; + } + + private void writeMap(Map map, boolean showType) throws IOException + { + if (writeOptionalReference(map)) + { + return; + } + + final Writer out = _out; + boolean referenced = _objsReferenced.containsKey(map); + + out.write('{'); + tabIn(out); + if (referenced) + { + writeId(getId(map)); + } + + if (showType) + { + if (referenced) + { + out.write(','); + newLine(out); + } + writeType(map, out); + } + + if (map.isEmpty()) + { + tabOut(out); + out.write('}'); + return; + } + + if (showType || referenced) + { + out.write(','); + newLine(out); + } + + out.write("\"@keys\":["); + tabIn(out); + Iterator i = map.keySet().iterator(); + + while (i.hasNext()) + { + writeCollectionElement(i.next()); + + if (i.hasNext()) + { + out.write(','); + newLine(out); + } + } + + tabOut(out); + out.write("],"); + newLine(out); + out.write("\"@items\":["); + tabIn(out); + i = map.values().iterator(); + + while (i.hasNext()) + { + writeCollectionElement(i.next()); + + if (i.hasNext()) + { + out.write(','); + newLine(out); + } + } + + tabOut(out); + out.write(']'); + tabOut(out); + out.write('}'); + } + + /** + * Write an element that is contained in some type of collection or Map. + * @param o Collection element to output in JSON format. + * @throws IOException if an error occurs writing to the output stream. + */ + private void writeCollectionElement(Object o) throws IOException + { + if (o == null) + { + _out.write("null"); + } + else if (o instanceof Boolean || o instanceof Long || o instanceof Double) + { + _out.write(o.toString()); + } + else if (o instanceof String) + { + writeJsonUtf8String((String) o, _out); + } + else + { + writeImpl(o, true); + } + } + + /** + * @param obj Object to be written in JSON format + * @param showType boolean true means show the "@type" field, false + * eliminates it. Many times the type can be dropped because it can be + * inferred from the field or array type. + * @throws IOException if an error occurs writing to the output stream. + */ + private void writeObject(Object obj, boolean showType) throws IOException + { + if (writeIfMatching(obj, showType, _out)) + { + return; + } + + if (writeOptionalReference(obj)) + { + return; + } + + final Writer out = _out; + + out.write('{'); + tabIn(out); + boolean referenced = _objsReferenced.containsKey(obj); + if (referenced) + { + writeId(getId(obj)); + } + + ClassMeta classInfo = getDeepDeclaredFields(obj.getClass()); + + if (referenced && showType) + { + out.write(','); + newLine(out); + } + + if (showType) + { + writeType(obj, out); + } + + boolean first = !showType; + if (referenced && !showType) + { + first = false; + } + + Map> fieldSpecifiers = (Map) _args.get().get(FIELD_SPECIFIERS); + List externallySpecifiedFields = getFieldsUsingSpecifier(obj.getClass(), fieldSpecifiers); + if (externallySpecifiedFields != null) + { // Caller is using associating a class name to a set of fields for the given class (allows field reductions) + for (Field field : externallySpecifiedFields) + { // Not currently supporting overwritten field names in hierarchy when using external field specifier + first = writeField(obj, out, first, field.getName(), field); + } + } + else + { // Reflectively use fields, skipping transient and static fields + for (Map.Entry entry : classInfo.entrySet()) + { + String fieldName = entry.getKey(); + Field field = entry.getValue(); + first = writeField(obj, out, first, fieldName, field); + } + } + + tabOut(out); + out.write('}'); + } + + private boolean writeField(Object obj, Writer out, boolean first, String fieldName, Field field) throws IOException + { + if ((field.getModifiers() & Modifier.TRANSIENT) != 0) + { // Do not write transient fields + return first; + } + if (first) + { + first = false; + } + else + { + out.write(','); + newLine(out); + } + + writeJsonUtf8String(fieldName, out); + out.write(':'); + + Object o; + try + { + o = field.get(obj); + } + catch (Exception ignored) + { + o = null; + } + + if (o == null) + { // don't quote null + out.write("null"); + return first; + } + + Class type = field.getType(); + boolean forceType = o.getClass() != type; // If types are not exactly the same, write "@type" field + + if (JsonReader.isPrimitive(type)) + { + writePrimitive(o); + } + else if (writeIfMatching(o, forceType, out)) { } + else + { + writeImpl(o, forceType || alwaysShowType()); + } + return first; + } + + /** + * Write out special characters "\b, \f, \t, \n, \r", as such, backslash as \\ + * quote as \" and values less than an ASCII space (20hex) as "\\u00xx" format, + * characters in the range of ASCII space to a '~' as ASCII, and anything higher in UTF-8. + * + * @param s String to be written in utf8 format on the output stream. + * @throws IOException if an error occurs writing to the output stream. + */ + public static void writeJsonUtf8String(String s, Writer out) throws IOException + { + out.write('\"'); + int len = s.length(); + + for (int i = 0; i < len; i++) + { + char c = s.charAt(i); + + if (c < ' ') + { // Anything less than ASCII space, write either in \\u00xx form, or the special \t, \n, etc. form + if (c == '\b') + { + out.write("\\b"); + } + else if (c == '\t') + { + out.write("\\t"); + } + else if (c == '\n') + { + out.write("\\n"); + } + else if (c == '\f') + { + out.write("\\f"); + } + else if (c == '\r') + { + out.write("\\r"); + } + else + { + String hex = Integer.toHexString(c); + out.write("\\u"); + int pad = 4 - hex.length(); + for (int k = 0; k < pad; k++) + { + out.write('0'); + } + out.write(hex); + } + } + else if (c == '\\' || c == '"') + { + out.write('\\'); + out.write(c); + } + else + { // Anything else - write in UTF-8 form (multi-byte encoded) (OutputStreamWriter is UTF-8) + out.write(c); + } + } + out.write('\"'); + } + + /** + * @param c Class instance + * @return ClassMeta which contains fields of class. The results are cached internally for performance + * when called again with same Class. + */ + static ClassMeta getDeepDeclaredFields(Class c) + { + ClassMeta classInfo = _classMetaCache.get(c.getName()); + if (classInfo != null) + { + return classInfo; + } + + classInfo = new ClassMeta(); + Class curr = c; + + while (curr != null) + { + try + { + Field[] local = curr.getDeclaredFields(); + + for (Field field : local) + { + if ((field.getModifiers() & Modifier.STATIC) == 0) + { // speed up: do not process static fields. + if (!field.isAccessible()) + { + try + { + field.setAccessible(true); + } + catch (Exception ignored) { } + } + if (classInfo.containsKey(field.getName())) + { + classInfo.put(curr.getName() + '.' + field.getName(), field); + } + else + { + classInfo.put(field.getName(), field); + } + } + } + } + catch (ThreadDeath t) + { + throw t; + } + catch (Throwable ignored) { } + + curr = curr.getSuperclass(); + } + + _classMetaCache.put(c.getName(), classInfo); + return classInfo; + } + + public void flush() + { + try + { + if (_out != null) + { + _out.flush(); + } + } + catch (Exception ignored) { } + } + + public void close() + { + try + { + _out.close(); + } + catch (Exception ignore) { } + } + + private String getId(Object o) + { + if (o instanceof JsonObject) + { + long id = ((JsonObject) o).id; + if (id != -1) + { + return String.valueOf(id); + } + } + Long id = _objsReferenced.get(o); + return id == null ? null : Long.toString(id); + } +} diff --git a/src/main/io/cread/teamcity/JSONMonitorController.java b/src/main/io/cread/teamcity/JSONMonitorController.java index b543980..52aab82 100644 --- a/src/main/io/cread/teamcity/JSONMonitorController.java +++ b/src/main/io/cread/teamcity/JSONMonitorController.java @@ -9,6 +9,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.sql.Time; import java.util.List; import java.util.Date; @@ -52,6 +53,8 @@ public ModelAndView handleRequest(HttpServletRequest request, HttpServletRespons SBuild latestBuild = buildType.getLastChangesStartedBuild(); if (latestBuild != null) { + String triggeredBy = latestBuild.getTriggeredBy().getAsString(); + String statusText = latestBuild.getStatusDescriptor().getText(); String userName = ""; ResponsibilityEntry buildTypeResponsibility = responsibilityFacade.findBuildTypeResponsibility(buildType); @@ -61,14 +64,15 @@ public ModelAndView handleRequest(HttpServletRequest request, HttpServletRespons } long duration = -1; + long finishTime = 0; long secondsSinceFinished = -1; if (!latestBuild.isFinished()) { duration = latestBuild.getDuration(); } else { - Date finishDate = latestBuild.getFinishDate(); - if (finishDate != null) { + finishTime = latestBuild.getFinishDate().getTime(); + if (finishTime > 0) { long now = new Date().getTime(); - secondsSinceFinished = (now - finishDate.getTime()) / 1000; + secondsSinceFinished = (now - finishTime) / 1000; } } @@ -79,7 +83,10 @@ public ModelAndView handleRequest(HttpServletRequest request, HttpServletRespons userName, latestBuild.getProjectExternalId(), duration, - secondsSinceFinished + secondsSinceFinished, + finishTime, + triggeredBy, + statusText )); } } diff --git a/src/main/io/cread/teamcity/JSONViewState.java b/src/main/io/cread/teamcity/JSONViewState.java index 2f21344..be703ba 100644 --- a/src/main/io/cread/teamcity/JSONViewState.java +++ b/src/main/io/cread/teamcity/JSONViewState.java @@ -1,5 +1,8 @@ package io.cread.teamcity; +import cedarsoftware.util.io.JsonWriter; + +import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -20,19 +23,27 @@ public String renderJobsList() { if (jobs.size() > 0) { for (JobState job : jobs) { - data.append("{\"name\":\"").append(job.name) - .append("\",\"url\":\"").append(rootURL).append("/viewType.html?buildTypeId=").append(job.id) - .append("\",\"responsible\":\"").append(job.responsible) - .append("\",\"project\":\"").append(job.project); - if (job.buildDuration >= 0) { - data.append("\",\"buildDuration\":\"").append(job.buildDuration); - } - if (job.secondsSinceFinished >= 0) { - data.append("\",\"secondsSinceFinished\":\"").append(job.secondsSinceFinished); + try { + data.append("{\"name\":").append(JsonWriter.objectToJson(job.name)); + data.append(",\"url\":\"").append(rootURL).append("/viewType.html?buildTypeId=").append(job.id).append("\""); + data.append(",\"responsible\":\"").append(job.responsible).append("\""); + data.append(",\"project\":\"").append(job.project).append("\""); + if (job.buildDuration >= 0) { + data.append(",\"buildDuration\":\"").append(job.buildDuration).append("\""); + } + if (job.secondsSinceFinished >= 0) { + data.append(",\"secondsSinceFinished\":\"").append(job.secondsSinceFinished).append("\""); + data.append(",\"finishTime\":\"").append(job.finishTime).append("\""); + data.append(",\"statusText\":").append(JsonWriter.objectToJson(job.statusText)); + } + data.append(",\"color\":\"").append(job.color).append("\""); + data.append(",\"triggeredBy\":").append(JsonWriter.objectToJson(job.triggeredBy)); + data.append("},"); + } catch(IOException e) { + e.printStackTrace(); } - data.append("\",\"color\":\"").append(job.color).append("\"},"); } - data.deleteCharAt(data.length() - 1); + data.deleteCharAt(data.length() - 1); // The trailing comma } else { data.append("{}"); } diff --git a/src/main/io/cread/teamcity/JobState.java b/src/main/io/cread/teamcity/JobState.java index eb1da06..4d9bd4b 100644 --- a/src/main/io/cread/teamcity/JobState.java +++ b/src/main/io/cread/teamcity/JobState.java @@ -1,5 +1,7 @@ package io.cread.teamcity; +import java.util.Date; + public class JobState { public final String name; public final String id; @@ -9,8 +11,12 @@ public class JobState { public final long buildDuration; public final long secondsSinceFinished; public final String color; + public final long finishTime; + public final String triggeredBy; + public final String statusText; - public JobState(String name, String id, String status, String responsible, String project, long buildDuration, long secondsSinceFinished) { + public JobState(String name, String id, String status, String responsible, String project, long buildDuration, + long secondsSinceFinished, long finishTime, String triggeredBy, String statusText) { this.name = name; this.id = id; this.status = status; @@ -19,5 +25,8 @@ public JobState(String name, String id, String status, String responsible, Strin this.project = project; this.secondsSinceFinished = secondsSinceFinished; this.color = ("SUCCESS".equals(status)) ? "blue" : "red"; + this.finishTime = finishTime; + this.triggeredBy = triggeredBy; + this.statusText = statusText; } } diff --git a/src/tests/io/cread/teamcity/JSONViewStateTests.java b/src/tests/io/cread/teamcity/JSONViewStateTests.java index 39db515..61ff6f6 100644 --- a/src/tests/io/cread/teamcity/JSONViewStateTests.java +++ b/src/tests/io/cread/teamcity/JSONViewStateTests.java @@ -2,6 +2,10 @@ import junit.framework.TestCase; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + public class JSONViewStateTests extends TestCase { JSONViewState state; @@ -16,26 +20,31 @@ public void testShouldRenderEmptyListIfNoJobs() { } public void testShouldBeAbleToRenderMultipleJobDetails() { - state.addJob(new JobState("Job1", "bt3", "ERROR", "buildadmin", "Big Project", 100L, 300L)); - state.addJob(new JobState("Job2", "bt5", "SUCCESS", "jdoe", "Medium Project", 200L, 400L)); - + long finishTime1 = 0; + try { + finishTime1 = new SimpleDateFormat("yyyy-MM-dd").parse("2011-01-01").getTime(); + } catch (ParseException e) { + e.printStackTrace(); + } + state.addJob(new JobState("Job1", "bt3", "ERROR", "buildadmin", "Big Project", 100L, 300L, finishTime1, "", "")); + state.addJob(new JobState("Job2", "bt5", "SUCCESS", "jdoe", "Medium Project", 200L, 400L, 0, "", "")); assertEquals("\"jobs\":[" + - "{\"name\":\"Job1\",\"url\":\"http://localhost:8111/viewType.html?buildTypeId=bt3\",\"responsible\":\"buildadmin\",\"project\":\"Big Project\",\"buildDuration\":\"100\",\"secondsSinceFinished\":\"300\",\"color\":\"red\"}," + - "{\"name\":\"Job2\",\"url\":\"http://localhost:8111/viewType.html?buildTypeId=bt5\",\"responsible\":\"jdoe\",\"project\":\"Medium Project\",\"buildDuration\":\"200\",\"secondsSinceFinished\":\"400\",\"color\":\"blue\"}" + + "{\"name\":\"Job1\",\"url\":\"http://localhost:8111/viewType.html?buildTypeId=bt3\",\"responsible\":\"buildadmin\",\"project\":\"Big Project\",\"buildDuration\":\"100\",\"secondsSinceFinished\":\"300\",\"finishTime\":\"1293861600000\",\"statusText\":\"\",\"color\":\"red\",\"triggeredBy\":\"\"}," + + "{\"name\":\"Job2\",\"url\":\"http://localhost:8111/viewType.html?buildTypeId=bt5\",\"responsible\":\"jdoe\",\"project\":\"Medium Project\",\"buildDuration\":\"200\",\"secondsSinceFinished\":\"400\",\"finishTime\":\"0\",\"statusText\":\"\",\"color\":\"blue\",\"triggeredBy\":\"\"}" + "],", state.renderJobsList()); } public void testShouldBeAbleToRenderSingleJobDetails() { - state.addJob(new JobState("Job1", "bt3", "ERROR", "rstevens", "Little Project", 300L, 400L)); + state.addJob(new JobState("Job1", "bt3", "ERROR", "rstevens", "Little Project", 300L, 400L, 0, "Some Name", "")); assertEquals("\"jobs\":[" + - "{\"name\":\"Job1\",\"url\":\"http://localhost:8111/viewType.html?buildTypeId=bt3\",\"responsible\":\"rstevens\",\"project\":\"Little Project\",\"buildDuration\":\"300\",\"secondsSinceFinished\":\"400\",\"color\":\"red\"}" + + "{\"name\":\"Job1\",\"url\":\"http://localhost:8111/viewType.html?buildTypeId=bt3\",\"responsible\":\"rstevens\",\"project\":\"Little Project\",\"buildDuration\":\"300\",\"secondsSinceFinished\":\"400\",\"finishTime\":\"0\",\"statusText\":\"\",\"color\":\"red\",\"triggeredBy\":\"Some Name\"}" + "],", state.renderJobsList()); } public void testShouldNotAddDuplicates() { - JobState job = new JobState("Job1", "bt3", "SUCCESS", "cread", "Stuff", -1, -1); + JobState job = new JobState("Job1", "bt3", "SUCCESS", "cread", "Stuff", -1L, -1L, 0, "", ""); state.addJob(job); @@ -47,10 +56,35 @@ public void testShouldNotAddDuplicates() { } public void testShouldNotRenderNoElapsedSeconds() { - state.addJob(new JobState("Job1", "bt3", "ERROR", "rstevens", "Little Project", -1L, -1)); + state.addJob(new JobState("Job1", "bt3", "ERROR", "rstevens", "Little Project", -1L, -1L, 0, "", "")); + + assertEquals("\"jobs\":[" + + "{\"name\":\"Job1\",\"url\":\"http://localhost:8111/viewType.html?buildTypeId=bt3\",\"responsible\":\"rstevens\",\"project\":\"Little Project\",\"color\":\"red\",\"triggeredBy\":\"\"}" + + "],", state.renderJobsList()); + } + + public void testShouldEscapeMetaCharsInStatusText() { + state.addJob(new JobState("Job1", "bt3", "ERROR", "rstevens", "Little Project", 5L, 6L, 0, "", "Status\nWith\nNewlines")); + + assertEquals("\"jobs\":[" + + "{\"name\":\"Job1\",\"url\":\"http://localhost:8111/viewType.html?buildTypeId=bt3\",\"responsible\":\"rstevens\",\"project\":\"Little Project\",\"buildDuration\":\"5\",\"secondsSinceFinished\":\"6\",\"finishTime\":\"0\",\"statusText\":\"Status\\nWith\\nNewlines\",\"color\":\"red\",\"triggeredBy\":\"\"}" + + "],", state.renderJobsList()); + } + + public void testShouldEscapeMetaCharsInTriggeredBy() { + state.addJob(new JobState("Job1", "bt3", "ERROR", "rstevens", "Little Project", 7L, 8L, 0, "Cute \":-)\" Name", "")); assertEquals("\"jobs\":[" + - "{\"name\":\"Job1\",\"url\":\"http://localhost:8111/viewType.html?buildTypeId=bt3\",\"responsible\":\"rstevens\",\"project\":\"Little Project\",\"color\":\"red\"}" + + "{\"name\":\"Job1\",\"url\":\"http://localhost:8111/viewType.html?buildTypeId=bt3\",\"responsible\":\"rstevens\",\"project\":\"Little Project\",\"buildDuration\":\"7\",\"secondsSinceFinished\":\"8\",\"finishTime\":\"0\",\"statusText\":\"\",\"color\":\"red\",\"triggeredBy\":\"Cute \\\":-)\\\" Name\"}" + "],", state.renderJobsList()); } + + public void testShouldEscapeMetaCharsInName() { + state.addJob(new JobState("First\"{Job", "bt3", "ERROR", "rstevens", "Little Project", 5L, 6L, 0, "", "Status\nWith\nNewlines")); + + assertEquals("\"jobs\":[" + + "{\"name\":\"First\\\"{Job\",\"url\":\"http://localhost:8111/viewType.html?buildTypeId=bt3\",\"responsible\":\"rstevens\",\"project\":\"Little Project\",\"buildDuration\":\"5\",\"secondsSinceFinished\":\"6\",\"finishTime\":\"0\",\"statusText\":\"Status\\nWith\\nNewlines\",\"color\":\"red\",\"triggeredBy\":\"\"}" + + "],", state.renderJobsList()); + } + }