Java tutorial
/* * Copyright (C) 2013 salesforce.com, inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.auraframework.util.json; import java.io.DataOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.math.BigDecimal; import java.text.SimpleDateFormat; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.TimeZone; import javax.annotation.Nonnull; import org.auraframework.util.AuraTextUtil; import org.auraframework.util.UncloseableOutputStream; import org.auraframework.util.json.Json.Serialization.ReferenceType; import org.auraframework.util.json.Json.Serialization.ReferenceScope; import com.google.common.base.Charsets; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.io.CountingOutputStream; /** * java -> javascript encoder. * * May or may not follow the official JSON (JavaScript Object Notation) * standard. It handles serializing all the basics, numbers, strings, booleans, * arrays, and maps as well as some common SFDC data structures like * PicklistItem and anything that implements {@link JsonSerializable}. Some * significant consumers of the output are inline editing * <ol> * <li>Java null reference: JS null value * <li>Java Map: JS Object * <li>Java List: JS Array * <li>Java Object: object.toArray() * </ol> * * NOTE: the code for handling the stacks is rather more complicated to maintain * performance. The problem is that {@link #writeMapBegin()} and * {@link #writeArrayBegin()} are called hundreds of thousands of times a * second, meaning that creating and discarding objects for each one is too * expensive. This means that we use a separate stack in the case that we are * not formatting to avoid the allocation of the object. * * @see <a * href="https://sites.google.com/a/salesforce.com/user-interface/documentation/json">SFDC * json documentation</a> * @since 144 */ public class Json { @SuppressWarnings("serial") public static class JsonException extends RuntimeException { public JsonException(String message) { super(message); } } public static final String MIME_TYPE = "application/json"; private final static String REF_INDICATOR = "serRefId"; private final static String ID_INDICATOR = "serId"; private final static String VALUE = "value"; public enum IndentType { BRACE(true, " "), SQUARE(true, " "), PARAM(true, ""), COMMENT(false, " * "); private boolean separated; private String indent; private IndentType(boolean separated, String indent) { this.separated = separated; this.indent = indent; } /** * Determines if this instance is separated. * * @return The separated. */ public boolean isSeparated() { return this.separated; } /** * Gets the indent for this instance. * * @return The indent. */ public String getIndent() { return this.indent; } } /** * A class to track indents when formatting json. */ private static class IndentEntry { public IndentEntry(IndentType type, String indent) { this.type = type; this.first = true; this.indent = indent + type.getIndent(); } private final IndentType type; private boolean first; private final String indent; public boolean needSeparator() { if (!this.type.isSeparated()) { throw new JsonException("Cannot use separator on " + this.type); } if (this.first) { this.first = false; return false; } return true; } public String getIndent() { return this.indent; } public IndentType getType() { return this.type; } } private final JsonSerializationContext serializationContext; private final Map<Object, Integer> actionMap; private final Map<Object, Integer> requestMap; private int lastRefId = 0; private final Appendable out; private final ArrayDeque<IndentEntry> indentStack = new ArrayDeque<IndentEntry>(); private final DataOutputStream binaryOutput; private CountingOutputStream currentBinaryStream; private long currentBinaryStreamLength; /** * Create a Json Serialization context object that maintains information * about one run. This Object is NOT thread-safe. It should only be used by * one thread at a time, and should not be reused. * * @param out The Appendable to write the serialized objects to. * @param format defaults to false. If true, the output will be multi-line * and indented. * @param refSupport If true, any objects annotated with @Serialization * will be serialized using serRefIds */ protected Json(Appendable out, boolean format, boolean refSupport) { this(out, null, new DefaultJsonSerializationContext(format, refSupport, false)); } protected Json(Appendable out, OutputStream binaryOutput, JsonSerializationContext context) { this.out = out; this.serializationContext = context; // No need to create the maps if we're not doing the ref stuff if (this.serializationContext.refSupport()) { actionMap = new IdentityHashMap<Object, Integer>(); requestMap = new IdentityHashMap<Object, Integer>(); } else { actionMap = null; requestMap = null; } // Set binaryOutput to a DataOutputStream if applicable; otherwise, null this.binaryOutput = binaryOutput == null ? null : (binaryOutput instanceof DataOutputStream ? (DataOutputStream) binaryOutput : new DataOutputStream(binaryOutput)); } // This annotations is only looked for on classes that implement // JsonSerializable @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Inherited public @interface Serialization { public enum ReferenceType { /** * This is the default. Just do normal json serialization */ NONE, /** * If a == b, just output serRefId=<the refId of the object> after * the first time it's output */ IDENTITY } ReferenceType referenceType() default ReferenceType.NONE; public enum ReferenceScope { /** * the reference is available for the entire request. */ REQUEST, /** * The reference is only internal tothe current action. */ ACTION } ReferenceScope referenceScope() default ReferenceScope.ACTION; } /** * Following are a bunch of static serialize methods. They mainly exist in * order to size a StringBuilder for you to some reasonable size. */ public static String serialize(Object obj) { StringBuilder sb = new StringBuilder(16); serialize(obj, sb); return sb.toString(); } /** * @param obj The thing to serialize * @param out The destination for the serialized form * @param format true if output should be indented and multiline for human readability (default = false) * @param refSupport true if @Serialization annotations should be honored (default = false) * @throws JsonSerializationException if there's an issue during serialization */ public static void serialize(Object obj, Appendable out, boolean format, boolean refSupport) { try { new Json(out, format, refSupport).writeValue(obj); } catch (IOException e) { throw new JsonSerializationException(e); } } public static String serialize(Object obj, boolean format, boolean refSupport) { StringBuilder sb = new StringBuilder(16); serialize(obj, sb, format, refSupport); return sb.toString(); } public static void serialize(Object obj, Appendable out) { try { new Json(out, false, false).writeValue(obj); } catch (IOException e) { throw new JsonSerializationException(e); } } public static void serialize(Object obj, Appendable out, JsonSerializationContext context) { try { new Json(out, null, context).writeValue(obj); } catch (IOException e) { throw new JsonSerializationException(e); } } public static String serialize(Object obj, JsonSerializationContext context) { try { StringBuilder sb = new StringBuilder(16); new Json(sb, null, context).writeValue(obj); return sb.toString(); } catch (IOException e) { throw new JsonSerializationException(e); } } public static String serialize(Object[] result) { StringBuilder sb = new StringBuilder(result.length * 16); serialize(result, sb); return sb.toString(); } public static String serialize(Collection<?> result) { StringBuilder sb = new StringBuilder(result.size() * 16); serialize(result, sb); return sb.toString(); } public static String serialize(Map<String, ?> result) { StringBuilder sb = new StringBuilder(result.size() * 32); serialize(result, sb); return sb.toString(); } /** * Creates a Json instance that is suitable for output streaming, one * element at a time. This can help avoid building up an entire JavaScript * AST all in memory before it gets serialized, which can help cut down * memory use.<br> * <br> * Note that you will need to call {@link #close()} when you are done to * ensure that all characters have been written out to the given * OutputStream. Otherwise, some characters might be missing at the end. * * @param out The OutputStream to write the serialized objects to using * UTF-8. This must not be null. * @param format Defaults to false. If true, the output will be multi-line * and indented. * @param refSupport If true, any objects annotated with @Serialization * will be serialized using serRefIds * @param nullValues When true, null values are written out when they exist * in arrays and map values. When false, array items and map * entries with null values are not serialized * @return A new Json instance that you can use for streaming to the given * OutputStream */ public static Json createJsonStream(@Nonnull OutputStream out, boolean format, boolean refSupport, boolean nullValues) { return createJsonStream(out, new DefaultJsonSerializationContext(format, refSupport, nullValues)); } /* * Creates a Json instance that is suitable for output streaming, one * element at a time. This can help avoid building up an entire JavaScript * AST all in memory before it gets serialized, which can help cut down * memory use.<br> * <br> * Note that you will need to call {@link #close()} when you are done to * ensure that all characters have been written out to the given * OutputStream. Otherwise, some characters might be missing at the end. * * @param out The OutputStream to write the serialized objects to using * UTF-8. This must not be null. * @param context The JSON serialization context to use for output * @return A new Json instance that you can use for streaming to the given * OutputStream */ public static Json createJsonStream(@Nonnull OutputStream out, JsonSerializationContext context) { if (out == null) { throw new IllegalArgumentException("out must not be null"); } final Writer writer = new OutputStreamWriter(out, Charsets.UTF_8); return new Json(writer, out, context); } /* * Creates a Json instance that is suitable for output streaming, one * element at a time. This can help avoid building up an entire JavaScript * AST all in memory before it gets serialized, which can help cut down * memory use.<br> * * @param out The Appendable to which to write the serialized objects. This must not be null. * @param context The JSON serialization context to use for output * @return A new Json instance that you can use for streaming to the given appendable */ public static Json createJsonStream(@Nonnull Appendable out, JsonSerializationContext context) { return new Json(out, null, context); } /** * This method is essentially here to provide type-checking for the * outermost map. * * @param jsonMap * @param out * @throws JsonSerializationException if there's an issue during * serialization */ public static void serialize(Map<String, ?> jsonMap, Appendable out) { serialize((Object) jsonMap, out); } /** * If refSupport is on, track the object for later equality/identity checks * * @param rs the reference scope for the object. * @param value the value for which we are storing a reference. * @return */ private Integer addReference(ReferenceScope rs, Object value) { int ret = ++lastRefId; Map<Object, Integer> m = (rs == ReferenceScope.ACTION) ? actionMap : requestMap; m.put(value, ret); return ret; } /** * If refSupport is on, clear a set of objects from the references. * * @param values the values to remove. */ public void clearReferences() { if (!serializationContext.refSupport()) { return; } actionMap.clear(); } /** * @param rs the scope for the reference * @param value the value for which we want a reference. * @return The refId previously assigned to the value, or null if none has been assigned yet. */ protected Integer getRefId(ReferenceScope rs, Object value) { switch (rs) { case ACTION: return actionMap.get(value); case REQUEST: return requestMap.get(value); } return null; } /** * Push an indent, with a type. * * See the notes on performance on the class above. * * This either creates a new IndentEntry and pushes a value on the boolean * stack, or it just uses the boolean stack (in the case of not pretty * printing). * * @param type the type of indent to push. */ public void pushIndent(IndentType type) { if (this.indentStack.isEmpty()) { this.indentStack.push(new IndentEntry(type, "")); } else { this.indentStack.push(new IndentEntry(type, this.indentStack.peek().getIndent())); } } /** * Check the indent type. * * See the notes on performance on the class above. * * @param type the type of indent that should be on the stack. * @param message the message for the throwable if it is wrong. */ public void checkIndent(IndentType type, String message) { if (this.indentStack.isEmpty() || !type.equals(this.indentStack.peek().getType())) { throw new JsonException(message); } } /** * Pop an indent off the stack. * * This both checks the type on the stack, and pulls it off. See the notes * on performance on the class above. */ public void popIndent(IndentType type, String message) { if (this.indentStack.isEmpty()) { throw new JsonException("Empty indent stack: " + message); } if (!type.equals(this.indentStack.pop().getType())) { throw new JsonException("Mismatched indent stack: " + message); } } /** * get the current indent. * * See the notes on performance on the class above. */ public String getIndent() { if (this.indentStack.isEmpty()) { return ""; } else { return this.indentStack.peek().getIndent(); } } /** * If formatting is enabled, indent, otherwise, no-op. * * @throws IOException */ public void writeIndent() throws IOException { if (isFormatting()) { out.append(getIndent()); } } /** * Write the beginning of a map. Make sure to call writeMapEnd later on. * * @throws IOException */ public void writeMapBegin() throws IOException { out.append('{'); writeBreak(); pushIndent(IndentType.BRACE); } /** * Write the end of a map. * * @throws IOException */ public void writeMapEnd() throws IOException { writeBreak(); popIndent(IndentType.BRACE, "Json.writeMapBegin must be called before calling Json.writeMapEnd"); writeIndent(); out.append('}'); } /** * Start a comment. * * This is probably not needed, but if we do want to write a multiline * comment in parts, you would call this function followed by multiple calls * to {@link #writeCommentBody(String)} followed by a call to * {@link #writeCommentEnd()} */ public void writeCommentBegin() throws IOException { if (isFormatting()) { writeBreak(); writeIndent(); out.append("/*"); } pushIndent(IndentType.COMMENT); } /** * Write out a comment end. */ public void writeCommentEnd() throws IOException { popIndent(IndentType.COMMENT, "Json.writeCommentEnd must be preceded by Json.writeCommentBegin"); if (isFormatting()) { writeBreak(); writeIndent(); out.append(" */"); } } /** * Write out a part of a comment body. * * This call must be preceded by {@link #writeCommentBegin()}. * * @param body the comment to write. */ public void writeCommentBody(String body) throws IOException { checkIndent(IndentType.COMMENT, "Json.writeCommentBody must be preceded by Json.writeCommentBegin"); if (isFormatting()) { writeBreak(); writeIndent(); out.append(body.replace("*/", "")); } } /** * Write out a comment. * * Note that these are not legal structures in JSON, perhaps we should have * a flag to turn off the writing of these for 'valid' JSON. Unfortunately, * we'd also have to rework our error handling. * * This could take the body and re-work newlines with the indent, but that * seems a good bit of work for little gain (i.e. the result would be * prettier, but who cares). * * @param body the body of the comment. */ public void writeComment(String body) throws IOException { writeCommentBegin(); writeCommentBody(body); writeCommentEnd(); } /** * Write the beginning of an array. Make sure to call writeArrayEnd later * on. * * @throws IOException */ public void writeArrayBegin() throws IOException { out.append('['); writeBreak(); pushIndent(IndentType.SQUARE); } /** * Write the end of an array. * * @throws IOException */ public void writeArrayEnd() throws IOException { writeBreak(); popIndent(IndentType.SQUARE, "Json.writeArrayBegin must be called before calling Json.writeArrayEnd"); writeIndent(); out.append(']'); } /** * If any entries have already been written to the current map/array (as * marked by the write*Begin methods), write a comma. If no elements have * yet been written, no-op. * * @throws IOException */ public void writeComma() throws IOException { if (!this.indentStack.isEmpty()) { if (this.indentStack.peek().needSeparator()) { out.append(","); writeBreak(); } } else { // ooh, why did this happen? throw new JsonException("writeComma with no writeArrayBegin or writeMapBegin"); } } public void writeMapSeparator() throws IOException { out.append(':'); } /** * Encode the given value and if m != null then perform Aura-specific * seialization that outputs extra information in the stream so that * references can be established by the JSON reader */ public void writeValue(Object value) throws IOException { JsonSerializer<Object> serializer = serializationContext.getSerializer(value); if (serializer == null) { throw new JsonSerializerNotFoundException(value); } ReferenceType rt = serializationContext.refSupport() ? serializer.getReferenceType(value) : ReferenceType.NONE; if (rt != ReferenceType.NONE) { Integer refId; if ((refId = getRefId(serializer.getReferenceScope(value), value)) != null) { // Output a simple reference writeMapBegin(); writeMapEntry(REF_INDICATOR, refId); writeMapEnd(); } else { refId = addReference(serializer.getReferenceScope(value), value); // Now manually output this 2-element map to avoid loop writeMapBegin(); writeMapEntry(ID_INDICATOR, refId); writeMapKey(VALUE); serializer.serialize(this, value); writeMapEnd(); } } else { serializer.serialize(this, value); } } /** * Just write the value.toString() out. Does not quote the value. * * @param value * @throws IOException */ public void writeLiteral(Object value) throws IOException { out.append(value.toString()); } /** * Quotes value.toString() and writes it. * * @param value * @throws IOException */ public void writeString(Object value) throws IOException { out.append('"'); out.append(AuraTextUtil.escapeForJSONString(value.toString())); out.append('"'); } /** * Write the date in the ISO-8601 format that's semi-standard in json2 (in * that it's in the comments) * * @param value * @throws IOException */ public void writeDate(Date value) throws IOException { out.append('"'); // Use the ISO DateTime format to write the date. synchronized (ISO8601FORMAT) { out.append(ISO8601FORMAT.format(value)); } out.append('"'); } private static final SimpleDateFormat ISO8601FORMAT; static { ISO8601FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); ISO8601FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); } /** * Write a map in a predictable order * * @param map * @throws IOException */ public void writeMap(Map<?, ?> map) throws IOException { writeMapBegin(); for (Object o : map.entrySet()) { Map.Entry<?, ?> entry = (Map.Entry<?, ?>) o; Object value = entry.getValue(); writeMapEntry(entry.getKey(), value); } writeMapEnd(); } /** * Write an array * * @param array * @throws IOException */ public void writeArray(Object[] array) throws IOException { writeArrayBegin(); for (Object o : array) { writeArrayEntry(o); } writeArrayEnd(); } /** * Write an array * * @param array * @throws IOException */ public void writeArray(Collection<?> array) throws IOException { writeArrayBegin(); for (Object o : array) { writeArrayEntry(o); } writeArrayEnd(); } /** * Write a value into the current array, and add leading commas and * formatting as appropriate. * * @param value * @throws IOException */ public void writeArrayEntry(Object value) throws IOException { if (value != null || serializationContext.isNullValueEnabled()) { writeComma(); writeIndent(); writeValue(value); } } /** * Write a value into the current Map, and add leading commas and formatting * as appropriate. * * @param key * @param value * @throws IOException */ public void writeMapEntry(Object key, Object value) throws IOException { writeMapEntry(key, value, null); } /** * Write a value into the current Map, and add leading commas and formatting * as appropriate. This version will consult its {@code type} parameter to * decide how to serialize null maps and arrays. * * @param key * @param value * @param type * @throws IOException */ public void writeMapEntry(Object key, Object value, String type) throws IOException { if (value == null && type != null) { try { Class<?> valueClass = Json.class.getClassLoader().loadClass(type.substring("java://".length())); if (Iterable.class.isAssignableFrom(valueClass)) { value = new ArrayList<Boolean>(0); } else if (Map.class.isAssignableFrom(valueClass)) { value = new HashMap<String, String>(0); } } catch (ClassNotFoundException e) { // Nevermind; treat "we don't know" as a non-list, non-map } } if (value != null || serializationContext.isNullValueEnabled()) { writeMapKey(key); writeValue(value); } } /** * Write a partial Map Entry -- everything except the value. This is useful * when the value requires special serialization. * * @param key * @throws IOException * @throws JsonSerializerNotFoundException if a serializer is not found for the key */ public void writeMapKey(Object key) throws IOException { writeComma(); writeIndent(); JsonSerializer<Object> serializer = serializationContext.getSerializer(key); if (serializer == null) { throw new JsonSerializerNotFoundException(key); } serializer.serialize(this, key); writeMapSeparator(); } /** * If formatting is on, write out a line break. * * @throws IOException */ public void writeBreak() throws IOException { if (isFormatting()) { out.append('\n'); } } /** * Start a binary stream using the given length and return an OutputStream * that the caller can write its binary data to.<br> * <br> * After calling this, write exactly the number of bytes specified to the * OutputStream returned by this method. After you do that, call * {@link #writeBinaryStreamEnd()}. * * @param streamLength The number of bytes that will exist in the output before the ending backtick * @return The OutputStream that the caller can write its output to */ public OutputStream writeBinaryStreamBegin(long streamLength) throws IOException { // If we are in the middle of another binary stream, then complain if (currentBinaryStream != null) { throw new IllegalStateException("Previous binary stream was not ended"); } // Signal our binary stream's beginning validateBinaryStreamEnabledAndWriteBacktick(); // Flush the output stream writer to push all pending characters onto the OutputStream if (out instanceof Writer) { ((Writer) out).flush(); } // A JSON+binary stream begins with the length as a big endian 64-bit long binaryOutput.writeLong(streamLength); currentBinaryStreamLength = streamLength; // Wrap our binaryOutput in a CountingOutputStream so that we can // validate the length later return currentBinaryStream = new CountingOutputStream(new UncloseableOutputStream(binaryOutput)); } private void validateBinaryStreamEnabledAndWriteBacktick() throws IOException { if (binaryOutput == null) { throw new IllegalStateException( "Binary streams are supported only when Json.createJsonStream is used with an InputStream"); } out.append('`'); } /** * Ends the current binary stream and ensures that the correct number of * bytes were written. If a discrepancy exists, then an * IllegalStateException gets thrown. */ public void writeBinaryStreamEnd() throws IOException { // Ensure that we are in a binary stream, and validate the length if (currentBinaryStream == null) { throw new IllegalStateException("Binary stream was not started"); } if (currentBinaryStreamLength != currentBinaryStream.getCount()) { throw new IllegalStateException("Length of the binary stream was written out as " + currentBinaryStreamLength + " bytes, but " + currentBinaryStream.getCount() + " bytes were actually written to the OutputStream returned by writeBinaryStreamBegin()"); } // Signal our binary stream's ending validateBinaryStreamEnabledAndWriteBacktick(); currentBinaryStream = null; currentBinaryStreamLength = 0; } /** * Writes out any buffered characters in the OutputStreamWriter to the * binary OutputStream and then closes the OutputStream.<br> * <br> * Note that this method does nothing if Json was not created with an * OutputStream, such as via * {@link #createJsonStream(OutputStream, boolean, boolean, boolean)}. */ public void close() throws IOException { if (binaryOutput != null) { if (out instanceof Writer) { // This also closes the underlying OutputStream ((Writer) out).close(); } else { binaryOutput.close(); } } } /** * Note: You should always try to use the write* methods instead, if at all * possible. * * @return the appendable for this run in case you want to write something * special to it. */ public Appendable getAppendable() { return out; } private boolean isFormatting() { return serializationContext.format(); } public JsonSerializationContext getSerializationContext() { return this.serializationContext; } /** * Resolve references and remove refId/serRefIds from the passed in object. * Useful when parsing json serialized with reference support by this class. * * @param config Must be a Map or List that consists only of other * Maps/Lists and primitives * @return A Map or List representing the data passed in with its references * resolved */ public static Object resolveRefs(Object config) { return resolveRefs(config, Maps.<Integer, Object>newHashMap(), null); } private static Object resolveRefs(Object config, Map<Integer, Object> cache) { return resolveRefs(config, cache, null); } @SuppressWarnings("unchecked") private static Object resolveRefs(Object config, Map<Integer, Object> cache, Object newValue) { if (config instanceof List) { List<Object> l = (List<Object>) config; List<Object> result; if (newValue != null) { result = (List<Object>) newValue; } else { result = Lists.newArrayListWithExpectedSize(l.size()); } for (Object o : l) { result.add(resolveRefs(o, cache)); } return result; } else if (config instanceof Map) { Map<String, Object> m = (Map<String, Object>) config; BigDecimal serId = (BigDecimal) m.get(ID_INDICATOR); if (serId != null) { Object value = m.get(VALUE); Object result = value instanceof List ? Lists.newArrayList() : Maps.newHashMap(); // We must cache the new item first because we could loop back // to this serId internally cache.put(serId.intValue(), result); return resolveRefs(value, cache, result); } BigDecimal serRefId = (BigDecimal) m.get(REF_INDICATOR); if (serRefId != null) { Object value = cache.get(serRefId.intValue()); // if there is no value we could throw here return value; } Map<String, Object> result; if (newValue != null) { result = (Map<String, Object>) newValue; } else { result = Maps.newHashMapWithExpectedSize(m.size()); } for (Entry<String, Object> e : m.entrySet()) { result.put(e.getKey(), resolveRefs(e.getValue(), cache)); } return result; } return config; } }