/**
* Copyright (c) 2012-2016 André Bargull
* Alle Rechte vorbehalten / All Rights Reserved. Use is subject to license terms.
*
* <https://github.com/anba/es6draft>
*/
package com.github.anba.es6draft.runtime.objects;
import static com.github.anba.es6draft.runtime.AbstractOperations.*;
import static com.github.anba.es6draft.runtime.internal.Errors.newSyntaxError;
import static com.github.anba.es6draft.runtime.internal.Errors.newTypeError;
import static com.github.anba.es6draft.runtime.internal.Properties.createProperties;
import static com.github.anba.es6draft.runtime.types.Undefined.UNDEFINED;
import java.util.HashSet;
import java.util.LinkedHashSet;
import com.github.anba.es6draft.parser.JSONParser;
import com.github.anba.es6draft.parser.ParserException;
import com.github.anba.es6draft.runtime.ExecutionContext;
import com.github.anba.es6draft.runtime.Realm;
import com.github.anba.es6draft.runtime.internal.Initializable;
import com.github.anba.es6draft.runtime.internal.Messages;
import com.github.anba.es6draft.runtime.internal.Properties.Attributes;
import com.github.anba.es6draft.runtime.internal.Properties.Function;
import com.github.anba.es6draft.runtime.internal.Properties.Prototype;
import com.github.anba.es6draft.runtime.internal.Properties.Value;
import com.github.anba.es6draft.runtime.internal.Strings;
import com.github.anba.es6draft.runtime.objects.number.NumberObject;
import com.github.anba.es6draft.runtime.types.BuiltinSymbol;
import com.github.anba.es6draft.runtime.types.Callable;
import com.github.anba.es6draft.runtime.types.Intrinsics;
import com.github.anba.es6draft.runtime.types.ScriptObject;
import com.github.anba.es6draft.runtime.types.Type;
import com.github.anba.es6draft.runtime.types.builtins.OrdinaryObject;
import com.github.anba.es6draft.runtime.types.builtins.StringObject;
/**
* <h1>24 Structured Data</h1><br>
* <h2>24.3 The JSON Object</h2>
* <ul>
* <li>24.3.2 JSON.parse (text [, reviver])
* <li>24.3.3 JSON.stringify (value [, replacer [, space]])
* </ul>
*/
public final class JSONObject extends OrdinaryObject implements Initializable {
/**
* Constructs a new JSON object.
*
* @param realm
* the realm object
*/
public JSONObject(Realm realm) {
super(realm);
}
@Override
public void initialize(Realm realm) {
createProperties(realm, this, Properties.class);
}
public enum Properties {
;
@Prototype
public static final Intrinsics __proto__ = Intrinsics.ObjectPrototype;
/**
* 24.3.1 JSON.parse ( text [ , reviver ] )
*
* @param cx
* the execution context
* @param thisValue
* the function this-value
* @param text
* the JSON text
* @param reviver
* the optional reviver argument
* @return the parsed JSON value
*/
@Function(name = "parse", arity = 2)
public static Object parse(ExecutionContext cx, Object thisValue, Object text, Object reviver) {
/* steps 1-2 */
String jtext = ToFlatString(cx, text);
/* steps 3-7 */
Object unfiltered;
try {
unfiltered = JSONParser.parse(cx, jtext);
} catch (ParserException e) {
throw newSyntaxError(cx, e, Messages.Key.JSONInvalidLiteral, e.getFormattedMessage(cx.getRealm()),
Integer.toString(e.getLine()), Integer.toString(e.getColumn()));
}
/* step 8 */
if (IsCallable(reviver)) {
OrdinaryObject root = ObjectCreate(cx, Intrinsics.ObjectPrototype);
String rootName = "";
boolean status = CreateDataProperty(cx, root, rootName, unfiltered);
assert status;
return InternalizeJSONProperty(cx, (Callable) reviver, root, rootName);
}
/* step 9 */
return unfiltered;
}
/**
* 24.3.2 JSON.stringify ( value [ , replacer [ , space ] ] )
*
* @param cx
* the execution context
* @param thisValue
* the function this-value
* @param value
* the value
* @param replacer
* the optional replacer argument
* @param space
* the optional space argument
* @return the JSON string
*/
@Function(name = "stringify", arity = 3)
public static Object stringify(ExecutionContext cx, Object thisValue, Object value, Object replacer,
Object space) {
/* steps 1-2 (not applicable) */
/* step 3 */
LinkedHashSet<String> propertyList = null;
Callable replacerFunction = null;
/* step 4 */
if (Type.isObject(replacer)) {
if (IsCallable(replacer)) {
replacerFunction = (Callable) replacer;
} else if (IsArray(cx, replacer)) {
propertyList = new LinkedHashSet<>();
ScriptObject objReplacer = (ScriptObject) replacer;
long len = ToLength(cx, Get(cx, objReplacer, "length"));
for (long k = 0; k < len; ++k) {
String item = null;
Object v = Get(cx, objReplacer, k);
if (Type.isString(v)) {
item = Type.stringValue(v).toString();
} else if (Type.isNumber(v)) {
item = ToString(Type.numberValue(v));
} else if (Type.isObject(v)) {
ScriptObject o = Type.objectValue(v);
if (o instanceof StringObject || o instanceof NumberObject) {
item = ToFlatString(cx, v);
}
}
if (item != null) {
propertyList.add(item);
}
}
}
}
/* step 5 */
if (Type.isObject(space)) {
ScriptObject o = Type.objectValue(space);
if (o instanceof NumberObject) {
space = ToNumber(cx, space);
} else if (o instanceof StringObject) {
space = ToString(cx, space);
}
}
/* steps 6-8 */
String gap;
if (Type.isNumber(space)) {
int nspace = (int) Math.max(0, Math.min(10, ToInteger(Type.numberValue(space))));
gap = Strings.repeat(' ', nspace);
} else if (Type.isString(space)) {
String sspace = Type.stringValue(space).toString();
gap = sspace.length() <= 10 ? sspace : sspace.substring(0, 10);
} else {
gap = "";
}
/* step 9 */
OrdinaryObject wrapper = ObjectCreate(cx, Intrinsics.ObjectPrototype);
/* steps 10-11 */
boolean status = CreateDataProperty(cx, wrapper, "", value);
assert status;
/* step 12 */
JSONSerializer serializer = new JSONSerializer(propertyList, replacerFunction, gap);
value = TransformJSONValue(cx, serializer, wrapper, "", value);
if (!IsJSONSerializable(value)) {
return UNDEFINED;
}
SerializeJSONValue(cx, serializer, value);
return serializer.result.toString();
}
/**
* 24.3.3 JSON [ @@toStringTag ]
*/
@Value(name = "[Symbol.toStringTag]", symbol = BuiltinSymbol.toStringTag,
attributes = @Attributes(writable = false, enumerable = false, configurable = true))
public static final String toStringTag = "JSON";
}
/**
* 24.3.1.1 Runtime Semantics: InternalizeJSONProperty( holder, name)
*
* @param cx
* the execution context
* @param reviver
* the reviver function
* @param holder
* the script object
* @param name
* the property key
* @return the result value
*/
private static Object InternalizeJSONProperty(ExecutionContext cx, Callable reviver, ScriptObject holder,
String name) {
/* steps 1-2 */
Object val = Get(cx, holder, name);
/* step 3 */
if (Type.isObject(val)) {
InternalizeJSONValue(cx, reviver, Type.objectValue(val));
}
/* step 4 */
return reviver.call(cx, holder, name, val);
}
/**
* 24.3.1.1 Runtime Semantics: InternalizeJSONProperty( holder, name)
*
* @param cx
* the execution context
* @param reviver
* the reviver function
* @param holder
* the script object
* @param name
* the property key
* @return the result value
*/
private static Object InternalizeJSONProperty(ExecutionContext cx, Callable reviver, ScriptObject holder,
long name) {
/* steps 1-2 */
Object val = Get(cx, holder, name);
/* step 3 */
if (Type.isObject(val)) {
InternalizeJSONValue(cx, reviver, Type.objectValue(val));
}
/* step 4 */
return reviver.call(cx, holder, ToString(name), val);
}
private static void InternalizeJSONValue(ExecutionContext cx, Callable reviver, ScriptObject val) {
/* InternalizeJSONProperty, step 3 */
/* steps 3.a-b */
boolean isArray = IsArray(cx, val);
/* steps 3.c-d */
if (isArray) {
/* step 3.c */
long len = ToLength(cx, Get(cx, val, "length"));
for (long i = 0; i < len; ++i) {
Object newElement = InternalizeJSONProperty(cx, reviver, val, i);
if (Type.isUndefined(newElement)) {
val.delete(cx, i);
} else {
CreateDataProperty(cx, val, i, newElement);
}
}
} else {
/* step 3.d */
for (String p : EnumerableOwnNames(cx, val)) {
Object newElement = InternalizeJSONProperty(cx, reviver, val, p);
if (Type.isUndefined(newElement)) {
val.delete(cx, p);
} else {
CreateDataProperty(cx, val, p, newElement);
}
}
}
}
private static final class JSONSerializer {
final HashSet<ScriptObject> stack;
final HashSet<String> propertyList;
final Callable replacerFunction;
final String gap;
final StringBuilder result = new StringBuilder();
int level = 0;
JSONSerializer(HashSet<String> propertyList, Callable replacerFunction, String gap) {
this.stack = new HashSet<>();
this.propertyList = propertyList;
this.replacerFunction = replacerFunction;
this.gap = gap;
}
}
/**
* 24.3.2.1 Runtime Semantics: SerializeJSONProperty (key, holder )
*
* @param cx
* the execution context
* @param serializer
* the serializer state
* @param holder
* the script object
* @param key
* the property key
* @param value
* the property value
* @return the transformed property value
*/
private static Object TransformJSONValue(ExecutionContext cx, JSONSerializer serializer, ScriptObject holder,
String key, Object value) {
/* steps 1-2 (not applicable) */
/* step 3 */
if (Type.isObject(value)) {
Object toJSON = Get(cx, Type.objectValue(value), "toJSON");
if (IsCallable(toJSON)) {
value = ((Callable) toJSON).call(cx, value, key);
}
}
/* step 4 */
if (serializer.replacerFunction != null) {
value = serializer.replacerFunction.call(cx, holder, key, value);
}
return value;
}
/**
* 24.3.2.1 Runtime Semantics: SerializeJSONProperty (key, holder )
*
* @param cx
* the execution context
* @param serializer
* the serializer state
* @param value
* the property value
*/
private static void SerializeJSONValue(ExecutionContext cx, JSONSerializer serializer, Object value) {
/* steps 1-4 (not applicable) */
/* steps 5-12 */
switch (Type.of(value)) {
case Null:
SerializeJSONNull(serializer);
return;
case Boolean:
SerializeJSONBoolean(serializer, Type.booleanValue(value));
return;
case String:
SerializeJSONString(serializer, Type.stringValue(value));
return;
case Number:
SerializeJSONNumber(serializer, Type.numberValue(value));
return;
case Object:
assert !IsCallable(value);
ScriptObject valueObj = Type.objectValue(value);
if (valueObj instanceof NumberObject) {
SerializeJSONNumber(serializer, ToNumber(cx, value));
} else if (valueObj instanceof StringObject) {
SerializeJSONString(serializer, ToString(cx, value));
} else if (valueObj instanceof BooleanObject) {
SerializeJSONBoolean(serializer, ((BooleanObject) valueObj).getBooleanData());
} else if (IsArray(cx, valueObj)) {
SerializeJSONArray(cx, serializer, valueObj);
} else {
SerializeJSONObject(cx, serializer, valueObj);
}
return;
case Undefined:
case Symbol:
case SIMD:
default:
throw new AssertionError();
}
}
private static void SerializeJSONNull(JSONSerializer serializer) {
serializer.result.append("null");
}
private static void SerializeJSONBoolean(JSONSerializer serializer, boolean b) {
serializer.result.append(b);
}
private static void SerializeJSONNumber(JSONSerializer serializer, double v) {
if (Double.isNaN(v) || Double.isInfinite(v)) {
serializer.result.append("null");
} else {
serializer.result.append(ToString(v));
}
}
private static void SerializeJSONString(JSONSerializer serializer, CharSequence string) {
QuoteJSONString(serializer.result, string.toString());
}
private static boolean IsJSONSerializable(Object value) {
switch (Type.of(value)) {
case Boolean:
case Null:
case String:
case Number:
return true;
case Object:
return !IsCallable(value);
case Undefined:
case Symbol:
case SIMD:
return false;
default:
throw new AssertionError();
}
}
/* @formatter:off */
private static final char[] HEXDIGITS = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
};
/* @formatter:on */
/**
* 24.3.2.2 Runtime Semantics: QuoteJSONString ( value )
*
* @param product
* the output string builder
* @param value
* the string
*/
private static void QuoteJSONString(StringBuilder product, String value) {
product.ensureCapacity(value.length() + 2);
/* step 1 */
product.append('"');
/* step 2 */
for (int i = 0, len = value.length(); i < len; ++i) {
char c = value.charAt(i);
switch (c) {
case '"':
case '\\':
product.append('\\').append(c);
break;
case '\b':
product.append('\\').append('b');
break;
case '\f':
product.append('\\').append('f');
break;
case '\n':
product.append('\\').append('n');
break;
case '\r':
product.append('\\').append('r');
break;
case '\t':
product.append('\\').append('t');
break;
default:
if (c < ' ') {
/* @formatter:off */
product.append('\\').append('u')
.append(HEXDIGITS[(c >> 12) & 0xf])
.append(HEXDIGITS[(c >> 8) & 0xf])
.append(HEXDIGITS[(c >> 4) & 0xf])
.append(HEXDIGITS[(c >> 0) & 0xf]);
/* @formatter:on */
} else {
product.append(c);
}
}
}
/* step 3 */
product.append('"');
/* step 4 (not applicable) */
}
/**
* 24.3.2.3 Runtime Semantics: SerializeJSONObject ( value )
*
* @param cx
* the execution context
* @param serializer
* the serializer state
* @param value
* the script object
*/
private static void SerializeJSONObject(ExecutionContext cx, JSONSerializer serializer, ScriptObject value) {
/* steps 1-2 */
if (!serializer.stack.add(value)) {
throw newTypeError(cx, Messages.Key.JSONCyclicValue);
}
/* steps 3-4 (not applicable) */
/* steps 5-6 */
Iterable<String> k;
if (serializer.propertyList != null) {
k = serializer.propertyList;
} else {
k = EnumerableOwnNames(cx, value);
}
/* step 7 (not applicable) */
/* steps 8-10 */
boolean isEmpty = true;
String gap = serializer.gap;
StringBuilder result = serializer.result;
result.append('{');
serializer.level += 1;
for (String p : k) {
// Inlined: SerializeJSONProperty
Object v = Get(cx, value, p);
v = TransformJSONValue(cx, serializer, value, p, v);
if (!IsJSONSerializable(v)) {
continue;
}
if (!isEmpty) {
result.append(',');
}
isEmpty = false;
if (!gap.isEmpty()) {
indent(serializer, result);
}
QuoteJSONString(result, p);
result.append(':');
if (!gap.isEmpty()) {
result.append(' ');
}
SerializeJSONValue(cx, serializer, v);
}
serializer.level -= 1;
if (!isEmpty && !gap.isEmpty()) {
indent(serializer, result);
}
result.append('}');
/* step 11 */
serializer.stack.remove(value);
/* steps 12-13 (not applicable) */
}
/**
* 24.3.2.4 Runtime Semantics: SerializeJSONArray( value )
*
* @param cx
* the execution context
* @param serializer
* the serializer state
* @param value
* the script array object
* @param stack
* the current stack
*/
private static void SerializeJSONArray(ExecutionContext cx, JSONSerializer serializer, ScriptObject value) {
/* steps 1-2 */
if (!serializer.stack.add(value)) {
throw newTypeError(cx, Messages.Key.JSONCyclicValue);
}
/* steps 3-5 (not applicable) */
/* steps 6-7 */
long len = ToLength(cx, Get(cx, value, "length"));
/* steps 8-11 */
String gap = serializer.gap;
StringBuilder result = serializer.result;
result.append('[');
if (len > 0) {
serializer.level += 1;
for (long index = 0; index < len; ++index) {
if (!gap.isEmpty()) {
indent(serializer, result);
}
// Inlined: SerializeJSONProperty
Object v = Get(cx, value, index);
v = TransformJSONValue(cx, serializer, value, ToString(index), v);
if (!IsJSONSerializable(v)) {
result.append("null");
} else {
SerializeJSONValue(cx, serializer, v);
}
if (index + 1 < len) {
result.append(',');
}
}
serializer.level -= 1;
if (!gap.isEmpty()) {
indent(serializer, result);
}
}
result.append(']');
/* step 12 */
serializer.stack.remove(value);
/* steps 13-14 (not applicable) */
}
private static void indent(JSONSerializer serializer, StringBuilder sb) {
int level = serializer.level;
String gap = serializer.gap;
sb.ensureCapacity(1 + level * gap.length());
sb.append('\n');
for (int i = 0; i < level; ++i) {
sb.append(gap);
}
}
}