| /* |
| * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| package jdk.nashorn.internal.objects; |
| |
| import static jdk.nashorn.internal.runtime.ECMAErrors.typeError; |
| import static jdk.nashorn.internal.runtime.ScriptRuntime.UNDEFINED; |
| |
| import java.lang.invoke.MethodHandle; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.IdentityHashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.concurrent.Callable; |
| import jdk.nashorn.api.scripting.JSObject; |
| import jdk.nashorn.api.scripting.ScriptObjectMirror; |
| import jdk.nashorn.internal.objects.annotations.Attribute; |
| import jdk.nashorn.internal.objects.annotations.Function; |
| import jdk.nashorn.internal.objects.annotations.ScriptClass; |
| import jdk.nashorn.internal.objects.annotations.Where; |
| import jdk.nashorn.internal.runtime.ConsString; |
| import jdk.nashorn.internal.runtime.JSONFunctions; |
| import jdk.nashorn.internal.runtime.JSType; |
| import jdk.nashorn.internal.runtime.PropertyMap; |
| import jdk.nashorn.internal.runtime.ScriptObject; |
| import jdk.nashorn.internal.runtime.arrays.ArrayLikeIterator; |
| import jdk.nashorn.internal.runtime.linker.Bootstrap; |
| import jdk.nashorn.internal.runtime.linker.InvokeByName; |
| |
| /** |
| * ECMAScript 262 Edition 5, Section 15.12 The NativeJSON Object |
| * |
| */ |
| @ScriptClass("JSON") |
| public final class NativeJSON extends ScriptObject { |
| private static final Object TO_JSON = new Object(); |
| |
| private static InvokeByName getTO_JSON() { |
| return Global.instance().getInvokeByName(TO_JSON, |
| new Callable<InvokeByName>() { |
| @Override |
| public InvokeByName call() { |
| return new InvokeByName("toJSON", ScriptObject.class, Object.class, Object.class); |
| } |
| }); |
| } |
| |
| private static final Object JSOBJECT_INVOKER = new Object(); |
| |
| private static MethodHandle getJSOBJECT_INVOKER() { |
| return Global.instance().getDynamicInvoker(JSOBJECT_INVOKER, |
| new Callable<MethodHandle>() { |
| @Override |
| public MethodHandle call() { |
| return Bootstrap.createDynamicCallInvoker(Object.class, Object.class, Object.class); |
| } |
| }); |
| } |
| |
| private static final Object REPLACER_INVOKER = new Object(); |
| |
| private static MethodHandle getREPLACER_INVOKER() { |
| return Global.instance().getDynamicInvoker(REPLACER_INVOKER, |
| new Callable<MethodHandle>() { |
| @Override |
| public MethodHandle call() { |
| return Bootstrap.createDynamicCallInvoker(Object.class, |
| Object.class, Object.class, Object.class, Object.class); |
| } |
| }); |
| } |
| |
| // initialized by nasgen |
| @SuppressWarnings("unused") |
| private static PropertyMap $nasgenmap$; |
| |
| private NativeJSON() { |
| // don't create me!! |
| throw new UnsupportedOperationException(); |
| } |
| |
| /** |
| * ECMA 15.12.2 parse ( text [ , reviver ] ) |
| * |
| * @param self self reference |
| * @param text a JSON formatted string |
| * @param reviver optional value: function that takes two parameters (key, value) |
| * |
| * @return an ECMA script value |
| */ |
| @Function(attributes = Attribute.NOT_ENUMERABLE, where = Where.CONSTRUCTOR) |
| public static Object parse(final Object self, final Object text, final Object reviver) { |
| return JSONFunctions.parse(text, reviver); |
| } |
| |
| /** |
| * ECMA 15.12.3 stringify ( value [ , replacer [ , space ] ] ) |
| * |
| * @param self self reference |
| * @param value ECMA script value (usually object or array) |
| * @param replacer either a function or an array of strings and numbers |
| * @param space optional parameter - allows result to have whitespace injection |
| * |
| * @return a string in JSON format |
| */ |
| @Function(attributes = Attribute.NOT_ENUMERABLE, where = Where.CONSTRUCTOR) |
| public static Object stringify(final Object self, final Object value, final Object replacer, final Object space) { |
| // The stringify method takes a value and an optional replacer, and an optional |
| // space parameter, and returns a JSON text. The replacer can be a function |
| // that can replace values, or an array of strings that will select the keys. |
| |
| // A default replacer method can be provided. Use of the space parameter can |
| // produce text that is more easily readable. |
| |
| final StringifyState state = new StringifyState(); |
| |
| // If there is a replacer, it must be a function or an array. |
| if (Bootstrap.isCallable(replacer)) { |
| state.replacerFunction = replacer; |
| } else if (isArray(replacer) || |
| isJSObjectArray(replacer) || |
| replacer instanceof Iterable || |
| (replacer != null && replacer.getClass().isArray())) { |
| |
| state.propertyList = new ArrayList<>(); |
| |
| final Iterator<Object> iter = ArrayLikeIterator.arrayLikeIterator(replacer); |
| |
| while (iter.hasNext()) { |
| String item = null; |
| final Object v = iter.next(); |
| |
| if (v instanceof String) { |
| item = (String) v; |
| } else if (v instanceof ConsString) { |
| item = v.toString(); |
| } else if (v instanceof Number || |
| v instanceof NativeNumber || |
| v instanceof NativeString) { |
| item = JSType.toString(v); |
| } |
| |
| if (item != null) { |
| state.propertyList.add(item); |
| } |
| } |
| } |
| |
| // If the space parameter is a number, make an indent |
| // string containing that many spaces. |
| |
| String gap; |
| |
| // modifiable 'space' - parameter is final |
| Object modSpace = space; |
| if (modSpace instanceof NativeNumber) { |
| modSpace = JSType.toNumber(JSType.toPrimitive(modSpace, Number.class)); |
| } else if (modSpace instanceof NativeString) { |
| modSpace = JSType.toString(JSType.toPrimitive(modSpace, String.class)); |
| } |
| |
| if (modSpace instanceof Number) { |
| final int indent = Math.min(10, JSType.toInteger(modSpace)); |
| if (indent < 1) { |
| gap = ""; |
| } else { |
| final StringBuilder sb = new StringBuilder(); |
| for (int i = 0; i < indent; i++) { |
| sb.append(' '); |
| } |
| gap = sb.toString(); |
| } |
| } else if (JSType.isString(modSpace)) { |
| final String str = modSpace.toString(); |
| gap = str.substring(0, Math.min(10, str.length())); |
| } else { |
| gap = ""; |
| } |
| |
| state.gap = gap; |
| |
| final ScriptObject wrapper = Global.newEmptyInstance(); |
| wrapper.set("", value, 0); |
| |
| return str("", wrapper, state); |
| } |
| |
| // -- Internals only below this point |
| |
| // stringify helpers. |
| |
| private static class StringifyState { |
| final Map<Object, Object> stack = new IdentityHashMap<>(); |
| |
| StringBuilder indent = new StringBuilder(); |
| String gap = ""; |
| List<String> propertyList = null; |
| Object replacerFunction = null; |
| } |
| |
| // Spec: The abstract operation Str(key, holder). |
| private static Object str(final Object key, final Object holder, final StringifyState state) { |
| assert holder instanceof ScriptObject || holder instanceof JSObject; |
| |
| Object value = getProperty(holder, key); |
| try { |
| if (value instanceof ScriptObject) { |
| final InvokeByName toJSONInvoker = getTO_JSON(); |
| final ScriptObject svalue = (ScriptObject)value; |
| final Object toJSON = toJSONInvoker.getGetter().invokeExact(svalue); |
| if (Bootstrap.isCallable(toJSON)) { |
| value = toJSONInvoker.getInvoker().invokeExact(toJSON, svalue, key); |
| } |
| } else if (value instanceof JSObject) { |
| final JSObject jsObj = (JSObject)value; |
| final Object toJSON = jsObj.getMember("toJSON"); |
| if (Bootstrap.isCallable(toJSON)) { |
| value = getJSOBJECT_INVOKER().invokeExact(toJSON, value); |
| } |
| } |
| |
| if (state.replacerFunction != null) { |
| value = getREPLACER_INVOKER().invokeExact(state.replacerFunction, holder, key, value); |
| } |
| } catch(Error|RuntimeException t) { |
| throw t; |
| } catch(final Throwable t) { |
| throw new RuntimeException(t); |
| } |
| final boolean isObj = (value instanceof ScriptObject); |
| if (isObj) { |
| if (value instanceof NativeNumber) { |
| value = JSType.toNumber(value); |
| } else if (value instanceof NativeString) { |
| value = JSType.toString(value); |
| } else if (value instanceof NativeBoolean) { |
| value = ((NativeBoolean)value).booleanValue(); |
| } |
| } |
| |
| if (value == null) { |
| return "null"; |
| } else if (Boolean.TRUE.equals(value)) { |
| return "true"; |
| } else if (Boolean.FALSE.equals(value)) { |
| return "false"; |
| } |
| |
| if (value instanceof String) { |
| return JSONFunctions.quote((String)value); |
| } else if (value instanceof ConsString) { |
| return JSONFunctions.quote(value.toString()); |
| } |
| |
| if (value instanceof Number) { |
| return JSType.isFinite(((Number)value).doubleValue()) ? JSType.toString(value) : "null"; |
| } |
| |
| final JSType type = JSType.of(value); |
| if (type == JSType.OBJECT) { |
| if (isArray(value) || isJSObjectArray(value)) { |
| return JA(value, state); |
| } else if (value instanceof ScriptObject || value instanceof JSObject) { |
| return JO(value, state); |
| } |
| } |
| |
| return UNDEFINED; |
| } |
| |
| // Spec: The abstract operation JO(value) serializes an object. |
| private static String JO(final Object value, final StringifyState state) { |
| assert value instanceof ScriptObject || value instanceof JSObject; |
| |
| if (state.stack.containsKey(value)) { |
| throw typeError("JSON.stringify.cyclic"); |
| } |
| |
| state.stack.put(value, value); |
| final StringBuilder stepback = new StringBuilder(state.indent.toString()); |
| state.indent.append(state.gap); |
| |
| final StringBuilder finalStr = new StringBuilder(); |
| final List<Object> partial = new ArrayList<>(); |
| final List<String> k = state.propertyList == null ? |
| Arrays.asList(getOwnKeys(value)) : state.propertyList; |
| |
| for (final Object p : k) { |
| final Object strP = str(p, value, state); |
| |
| if (strP != UNDEFINED) { |
| final StringBuilder member = new StringBuilder(); |
| |
| member.append(JSONFunctions.quote(p.toString())).append(':'); |
| if (!state.gap.isEmpty()) { |
| member.append(' '); |
| } |
| |
| member.append(strP); |
| partial.add(member); |
| } |
| } |
| |
| if (partial.isEmpty()) { |
| finalStr.append("{}"); |
| } else { |
| if (state.gap.isEmpty()) { |
| final int size = partial.size(); |
| int index = 0; |
| |
| finalStr.append('{'); |
| |
| for (final Object str : partial) { |
| finalStr.append(str); |
| if (index < size - 1) { |
| finalStr.append(','); |
| } |
| index++; |
| } |
| |
| finalStr.append('}'); |
| } else { |
| final int size = partial.size(); |
| int index = 0; |
| |
| finalStr.append("{\n"); |
| finalStr.append(state.indent); |
| |
| for (final Object str : partial) { |
| finalStr.append(str); |
| if (index < size - 1) { |
| finalStr.append(",\n"); |
| finalStr.append(state.indent); |
| } |
| index++; |
| } |
| |
| finalStr.append('\n'); |
| finalStr.append(stepback); |
| finalStr.append('}'); |
| } |
| } |
| |
| state.stack.remove(value); |
| state.indent = stepback; |
| |
| return finalStr.toString(); |
| } |
| |
| // Spec: The abstract operation JA(value) serializes an array. |
| private static Object JA(final Object value, final StringifyState state) { |
| assert value instanceof ScriptObject || value instanceof JSObject; |
| |
| if (state.stack.containsKey(value)) { |
| throw typeError("JSON.stringify.cyclic"); |
| } |
| |
| state.stack.put(value, value); |
| final StringBuilder stepback = new StringBuilder(state.indent.toString()); |
| state.indent.append(state.gap); |
| final List<Object> partial = new ArrayList<>(); |
| |
| final int length = JSType.toInteger(getLength(value)); |
| int index = 0; |
| |
| while (index < length) { |
| Object strP = str(index, value, state); |
| if (strP == UNDEFINED) { |
| strP = "null"; |
| } |
| partial.add(strP); |
| index++; |
| } |
| |
| final StringBuilder finalStr = new StringBuilder(); |
| if (partial.isEmpty()) { |
| finalStr.append("[]"); |
| } else { |
| if (state.gap.isEmpty()) { |
| final int size = partial.size(); |
| index = 0; |
| finalStr.append('['); |
| for (final Object str : partial) { |
| finalStr.append(str); |
| if (index < size - 1) { |
| finalStr.append(','); |
| } |
| index++; |
| } |
| |
| finalStr.append(']'); |
| } else { |
| final int size = partial.size(); |
| index = 0; |
| finalStr.append("[\n"); |
| finalStr.append(state.indent); |
| for (final Object str : partial) { |
| finalStr.append(str); |
| if (index < size - 1) { |
| finalStr.append(",\n"); |
| finalStr.append(state.indent); |
| } |
| index++; |
| } |
| |
| finalStr.append('\n'); |
| finalStr.append(stepback); |
| finalStr.append(']'); |
| } |
| } |
| |
| state.stack.remove(value); |
| state.indent = stepback; |
| |
| return finalStr.toString(); |
| } |
| |
| private static String[] getOwnKeys(final Object obj) { |
| if (obj instanceof ScriptObject) { |
| return ((ScriptObject)obj).getOwnKeys(false); |
| } else if (obj instanceof ScriptObjectMirror) { |
| return ((ScriptObjectMirror)obj).getOwnKeys(false); |
| } else if (obj instanceof JSObject) { |
| // No notion of "own keys" or "proto" for general JSObject! We just |
| // return all keys of the object. This will be useful for POJOs |
| // implementing JSObject interface. |
| return ((JSObject)obj).keySet().toArray(new String[0]); |
| } else { |
| throw new AssertionError("should not reach here"); |
| } |
| } |
| |
| private static Object getLength(final Object obj) { |
| if (obj instanceof ScriptObject) { |
| return ((ScriptObject)obj).getLength(); |
| } else if (obj instanceof JSObject) { |
| return ((JSObject)obj).getMember("length"); |
| } else { |
| throw new AssertionError("should not reach here"); |
| } |
| } |
| |
| private static boolean isJSObjectArray(final Object obj) { |
| return (obj instanceof JSObject) && ((JSObject)obj).isArray(); |
| } |
| |
| private static Object getProperty(final Object holder, final Object key) { |
| if (holder instanceof ScriptObject) { |
| return ((ScriptObject)holder).get(key); |
| } else if (holder instanceof JSObject) { |
| final JSObject jsObj = (JSObject)holder; |
| if (key instanceof Integer) { |
| return jsObj.getSlot((Integer)key); |
| } else { |
| return jsObj.getMember(Objects.toString(key)); |
| } |
| } else { |
| return new AssertionError("should not reach here"); |
| } |
| } |
| } |