1/*
2 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package jdk.nashorn.internal.objects;
27
28import static jdk.nashorn.internal.runtime.ECMAErrors.typeError;
29import static jdk.nashorn.internal.runtime.ScriptRuntime.UNDEFINED;
30
31import java.lang.invoke.MethodHandle;
32import java.util.ArrayList;
33import java.util.Arrays;
34import java.util.IdentityHashMap;
35import java.util.Iterator;
36import java.util.List;
37import java.util.Map;
38import java.util.Objects;
39import java.util.concurrent.Callable;
40import jdk.nashorn.api.scripting.JSObject;
41import jdk.nashorn.api.scripting.ScriptObjectMirror;
42import jdk.nashorn.internal.objects.annotations.Attribute;
43import jdk.nashorn.internal.objects.annotations.Function;
44import jdk.nashorn.internal.objects.annotations.ScriptClass;
45import jdk.nashorn.internal.objects.annotations.Where;
46import jdk.nashorn.internal.runtime.ConsString;
47import jdk.nashorn.internal.runtime.JSONFunctions;
48import jdk.nashorn.internal.runtime.JSType;
49import jdk.nashorn.internal.runtime.PropertyMap;
50import jdk.nashorn.internal.runtime.ScriptObject;
51import jdk.nashorn.internal.runtime.arrays.ArrayLikeIterator;
52import jdk.nashorn.internal.runtime.linker.Bootstrap;
53import jdk.nashorn.internal.runtime.linker.InvokeByName;
54
55/**
56 * ECMAScript 262 Edition 5, Section 15.12 The NativeJSON Object
57 *
58 */
59@ScriptClass("JSON")
60public final class NativeJSON extends ScriptObject {
61    private static final Object TO_JSON = new Object();
62
63    private static InvokeByName getTO_JSON() {
64        return Global.instance().getInvokeByName(TO_JSON,
65                new Callable<InvokeByName>() {
66                    @Override
67                    public InvokeByName call() {
68                        return new InvokeByName("toJSON", ScriptObject.class, Object.class, Object.class);
69                    }
70                });
71    }
72
73    private static final Object JSOBJECT_INVOKER = new Object();
74
75    private static MethodHandle getJSOBJECT_INVOKER() {
76        return Global.instance().getDynamicInvoker(JSOBJECT_INVOKER,
77                new Callable<MethodHandle>() {
78                    @Override
79                    public MethodHandle call() {
80                        return Bootstrap.createDynamicCallInvoker(Object.class, Object.class, Object.class);
81                    }
82                });
83    }
84
85    private static final Object REPLACER_INVOKER = new Object();
86
87    private static MethodHandle getREPLACER_INVOKER() {
88        return Global.instance().getDynamicInvoker(REPLACER_INVOKER,
89                new Callable<MethodHandle>() {
90                    @Override
91                    public MethodHandle call() {
92                        return Bootstrap.createDynamicCallInvoker(Object.class,
93                            Object.class, Object.class, Object.class, Object.class);
94                    }
95                });
96    }
97
98    // initialized by nasgen
99    @SuppressWarnings("unused")
100    private static PropertyMap $nasgenmap$;
101
102    private NativeJSON() {
103        // don't create me!!
104        throw new UnsupportedOperationException();
105    }
106
107    /**
108     * ECMA 15.12.2 parse ( text [ , reviver ] )
109     *
110     * @param self     self reference
111     * @param text     a JSON formatted string
112     * @param reviver  optional value: function that takes two parameters (key, value)
113     *
114     * @return an ECMA script value
115     */
116    @Function(attributes = Attribute.NOT_ENUMERABLE, where = Where.CONSTRUCTOR)
117    public static Object parse(final Object self, final Object text, final Object reviver) {
118        return JSONFunctions.parse(text, reviver);
119    }
120
121    /**
122     * ECMA 15.12.3 stringify ( value [ , replacer [ , space ] ] )
123     *
124     * @param self     self reference
125     * @param value    ECMA script value (usually object or array)
126     * @param replacer either a function or an array of strings and numbers
127     * @param space    optional parameter - allows result to have whitespace injection
128     *
129     * @return a string in JSON format
130     */
131    @Function(attributes = Attribute.NOT_ENUMERABLE, where = Where.CONSTRUCTOR)
132    public static Object stringify(final Object self, final Object value, final Object replacer, final Object space) {
133        // The stringify method takes a value and an optional replacer, and an optional
134        // space parameter, and returns a JSON text. The replacer can be a function
135        // that can replace values, or an array of strings that will select the keys.
136
137        // A default replacer method can be provided. Use of the space parameter can
138        // produce text that is more easily readable.
139
140        final StringifyState state = new StringifyState();
141
142        // If there is a replacer, it must be a function or an array.
143        if (Bootstrap.isCallable(replacer)) {
144            state.replacerFunction = replacer;
145        } else if (isArray(replacer) ||
146                isJSObjectArray(replacer) ||
147                replacer instanceof Iterable ||
148                (replacer != null && replacer.getClass().isArray())) {
149
150            state.propertyList = new ArrayList<>();
151
152            final Iterator<Object> iter = ArrayLikeIterator.arrayLikeIterator(replacer);
153
154            while (iter.hasNext()) {
155                String item = null;
156                final Object v = iter.next();
157
158                if (v instanceof String) {
159                    item = (String) v;
160                } else if (v instanceof ConsString) {
161                    item = v.toString();
162                } else if (v instanceof Number ||
163                        v instanceof NativeNumber ||
164                        v instanceof NativeString) {
165                    item = JSType.toString(v);
166                }
167
168                if (item != null) {
169                    state.propertyList.add(item);
170                }
171            }
172        }
173
174        // If the space parameter is a number, make an indent
175        // string containing that many spaces.
176
177        String gap;
178
179        // modifiable 'space' - parameter is final
180        Object modSpace = space;
181        if (modSpace instanceof NativeNumber) {
182            modSpace = JSType.toNumber(JSType.toPrimitive(modSpace, Number.class));
183        } else if (modSpace instanceof NativeString) {
184            modSpace = JSType.toString(JSType.toPrimitive(modSpace, String.class));
185        }
186
187        if (modSpace instanceof Number) {
188            final int indent = Math.min(10, JSType.toInteger(modSpace));
189            if (indent < 1) {
190                gap = "";
191            } else {
192                final StringBuilder sb = new StringBuilder();
193                for (int i = 0; i < indent; i++) {
194                    sb.append(' ');
195                }
196                gap = sb.toString();
197            }
198        } else if (JSType.isString(modSpace)) {
199            final String str = modSpace.toString();
200            gap = str.substring(0, Math.min(10, str.length()));
201        } else {
202            gap = "";
203        }
204
205        state.gap = gap;
206
207        final ScriptObject wrapper = Global.newEmptyInstance();
208        wrapper.set("", value, 0);
209
210        return str("", wrapper, state);
211    }
212
213    // -- Internals only below this point
214
215    // stringify helpers.
216
217    private static class StringifyState {
218        final Map<Object, Object> stack = new IdentityHashMap<>();
219
220        StringBuilder  indent = new StringBuilder();
221        String         gap = "";
222        List<String>   propertyList = null;
223        Object         replacerFunction = null;
224    }
225
226    // Spec: The abstract operation Str(key, holder).
227    private static Object str(final Object key, final Object holder, final StringifyState state) {
228        assert holder instanceof ScriptObject || holder instanceof JSObject;
229
230        Object value = getProperty(holder, key);
231        try {
232            if (value instanceof ScriptObject) {
233                final InvokeByName toJSONInvoker = getTO_JSON();
234                final ScriptObject svalue = (ScriptObject)value;
235                final Object toJSON = toJSONInvoker.getGetter().invokeExact(svalue);
236                if (Bootstrap.isCallable(toJSON)) {
237                    value = toJSONInvoker.getInvoker().invokeExact(toJSON, svalue, key);
238                }
239            } else if (value instanceof JSObject) {
240                final JSObject jsObj = (JSObject)value;
241                final Object toJSON = jsObj.getMember("toJSON");
242                if (Bootstrap.isCallable(toJSON)) {
243                    value = getJSOBJECT_INVOKER().invokeExact(toJSON, value);
244                }
245            }
246
247            if (state.replacerFunction != null) {
248                value = getREPLACER_INVOKER().invokeExact(state.replacerFunction, holder, key, value);
249            }
250        } catch(Error|RuntimeException t) {
251            throw t;
252        } catch(final Throwable t) {
253            throw new RuntimeException(t);
254        }
255        final boolean isObj = (value instanceof ScriptObject);
256        if (isObj) {
257            if (value instanceof NativeNumber) {
258                value = JSType.toNumber(value);
259            } else if (value instanceof NativeString) {
260                value = JSType.toString(value);
261            } else if (value instanceof NativeBoolean) {
262                value = ((NativeBoolean)value).booleanValue();
263            }
264        }
265
266        if (value == null) {
267            return "null";
268        } else if (Boolean.TRUE.equals(value)) {
269            return "true";
270        } else if (Boolean.FALSE.equals(value)) {
271            return "false";
272        }
273
274        if (value instanceof String) {
275            return JSONFunctions.quote((String)value);
276        } else if (value instanceof ConsString) {
277            return JSONFunctions.quote(value.toString());
278        }
279
280        if (value instanceof Number) {
281            return JSType.isFinite(((Number)value).doubleValue()) ? JSType.toString(value) : "null";
282        }
283
284        final JSType type = JSType.of(value);
285        if (type == JSType.OBJECT) {
286            if (isArray(value) || isJSObjectArray(value)) {
287                return JA(value, state);
288            } else if (value instanceof ScriptObject || value instanceof JSObject) {
289                return JO(value, state);
290            }
291        }
292
293        return UNDEFINED;
294    }
295
296    // Spec: The abstract operation JO(value) serializes an object.
297    private static String JO(final Object value, final StringifyState state) {
298        assert value instanceof ScriptObject || value instanceof JSObject;
299
300        if (state.stack.containsKey(value)) {
301            throw typeError("JSON.stringify.cyclic");
302        }
303
304        state.stack.put(value, value);
305        final StringBuilder stepback = new StringBuilder(state.indent.toString());
306        state.indent.append(state.gap);
307
308        final StringBuilder finalStr = new StringBuilder();
309        final List<Object>  partial  = new ArrayList<>();
310        final List<String>  k        = state.propertyList == null ?
311                Arrays.asList(getOwnKeys(value)) : state.propertyList;
312
313        for (final Object p : k) {
314            final Object strP = str(p, value, state);
315
316            if (strP != UNDEFINED) {
317                final StringBuilder member = new StringBuilder();
318
319                member.append(JSONFunctions.quote(p.toString())).append(':');
320                if (!state.gap.isEmpty()) {
321                    member.append(' ');
322                }
323
324                member.append(strP);
325                partial.add(member);
326            }
327        }
328
329        if (partial.isEmpty()) {
330            finalStr.append("{}");
331        } else {
332            if (state.gap.isEmpty()) {
333                final int size = partial.size();
334                int       index = 0;
335
336                finalStr.append('{');
337
338                for (final Object str : partial) {
339                    finalStr.append(str);
340                    if (index < size - 1) {
341                        finalStr.append(',');
342                    }
343                    index++;
344                }
345
346                finalStr.append('}');
347            } else {
348                final int size  = partial.size();
349                int       index = 0;
350
351                finalStr.append("{\n");
352                finalStr.append(state.indent);
353
354                for (final Object str : partial) {
355                    finalStr.append(str);
356                    if (index < size - 1) {
357                        finalStr.append(",\n");
358                        finalStr.append(state.indent);
359                    }
360                    index++;
361                }
362
363                finalStr.append('\n');
364                finalStr.append(stepback);
365                finalStr.append('}');
366            }
367        }
368
369        state.stack.remove(value);
370        state.indent = stepback;
371
372        return finalStr.toString();
373    }
374
375    // Spec: The abstract operation JA(value) serializes an array.
376    private static Object JA(final Object value, final StringifyState state) {
377        assert value instanceof ScriptObject || value instanceof JSObject;
378
379        if (state.stack.containsKey(value)) {
380            throw typeError("JSON.stringify.cyclic");
381        }
382
383        state.stack.put(value, value);
384        final StringBuilder stepback = new StringBuilder(state.indent.toString());
385        state.indent.append(state.gap);
386        final List<Object> partial = new ArrayList<>();
387
388        final int length = JSType.toInteger(getLength(value));
389        int index = 0;
390
391        while (index < length) {
392            Object strP = str(index, value, state);
393            if (strP == UNDEFINED) {
394                strP = "null";
395            }
396            partial.add(strP);
397            index++;
398        }
399
400        final StringBuilder finalStr = new StringBuilder();
401        if (partial.isEmpty()) {
402            finalStr.append("[]");
403        } else {
404            if (state.gap.isEmpty()) {
405                final int size = partial.size();
406                index = 0;
407                finalStr.append('[');
408                for (final Object str : partial) {
409                    finalStr.append(str);
410                    if (index < size - 1) {
411                        finalStr.append(',');
412                    }
413                    index++;
414                }
415
416                finalStr.append(']');
417            } else {
418                final int size = partial.size();
419                index = 0;
420                finalStr.append("[\n");
421                finalStr.append(state.indent);
422                for (final Object str : partial) {
423                    finalStr.append(str);
424                    if (index < size - 1) {
425                        finalStr.append(",\n");
426                        finalStr.append(state.indent);
427                    }
428                    index++;
429                }
430
431                finalStr.append('\n');
432                finalStr.append(stepback);
433                finalStr.append(']');
434            }
435        }
436
437        state.stack.remove(value);
438        state.indent = stepback;
439
440        return finalStr.toString();
441    }
442
443    private static String[] getOwnKeys(final Object obj) {
444        if (obj instanceof ScriptObject) {
445            return ((ScriptObject)obj).getOwnKeys(false);
446        } else if (obj instanceof ScriptObjectMirror) {
447            return ((ScriptObjectMirror)obj).getOwnKeys(false);
448        } else if (obj instanceof JSObject) {
449            // No notion of "own keys" or "proto" for general JSObject! We just
450            // return all keys of the object. This will be useful for POJOs
451            // implementing JSObject interface.
452            return ((JSObject)obj).keySet().toArray(new String[0]);
453        } else {
454            throw new AssertionError("should not reach here");
455        }
456    }
457
458    private static Object getLength(final Object obj) {
459        if (obj instanceof ScriptObject) {
460            return ((ScriptObject)obj).getLength();
461        } else if (obj instanceof JSObject) {
462            return ((JSObject)obj).getMember("length");
463        } else {
464            throw new AssertionError("should not reach here");
465        }
466    }
467
468    private static boolean isJSObjectArray(final Object obj) {
469        return (obj instanceof JSObject) && ((JSObject)obj).isArray();
470    }
471
472    private static Object getProperty(final Object holder, final Object key) {
473        if (holder instanceof ScriptObject) {
474            return ((ScriptObject)holder).get(key);
475        } else if (holder instanceof JSObject) {
476            final JSObject jsObj = (JSObject)holder;
477            if (key instanceof Integer) {
478                return jsObj.getSlot((Integer)key);
479            } else {
480                return jsObj.getMember(Objects.toString(key));
481            }
482        } else {
483            return new AssertionError("should not reach here");
484        }
485    }
486}
487