NativeJSON.java revision 1033:c1f651636d9c
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.concurrent.Callable;
39import jdk.nashorn.internal.objects.annotations.Attribute;
40import jdk.nashorn.internal.objects.annotations.Function;
41import jdk.nashorn.internal.objects.annotations.ScriptClass;
42import jdk.nashorn.internal.objects.annotations.Where;
43import jdk.nashorn.internal.runtime.ConsString;
44import jdk.nashorn.internal.runtime.JSONFunctions;
45import jdk.nashorn.internal.runtime.JSType;
46import jdk.nashorn.internal.runtime.PropertyMap;
47import jdk.nashorn.internal.runtime.ScriptFunction;
48import jdk.nashorn.internal.runtime.ScriptObject;
49import jdk.nashorn.internal.runtime.arrays.ArrayLikeIterator;
50import jdk.nashorn.internal.runtime.linker.Bootstrap;
51import jdk.nashorn.internal.runtime.linker.InvokeByName;
52
53/**
54 * ECMAScript 262 Edition 5, Section 15.12 The NativeJSON Object
55 *
56 */
57@ScriptClass("JSON")
58public final class NativeJSON extends ScriptObject {
59    private static final Object TO_JSON = new Object();
60
61    private static InvokeByName getTO_JSON() {
62        return Global.instance().getInvokeByName(TO_JSON,
63                new Callable<InvokeByName>() {
64                    @Override
65                    public InvokeByName call() {
66                        return new InvokeByName("toJSON", ScriptObject.class, Object.class, Object.class);
67                    }
68                });
69    }
70
71
72    private static final Object REPLACER_INVOKER = new Object();
73
74    private static MethodHandle getREPLACER_INVOKER() {
75        return Global.instance().getDynamicInvoker(REPLACER_INVOKER,
76                new Callable<MethodHandle>() {
77                    @Override
78                    public MethodHandle call() {
79                        return Bootstrap.createDynamicInvoker("dyn:call", Object.class,
80                            ScriptFunction.class, ScriptObject.class, Object.class, Object.class);
81                    }
82                });
83    }
84
85    // initialized by nasgen
86    @SuppressWarnings("unused")
87    private static PropertyMap $nasgenmap$;
88
89    private NativeJSON() {
90        // don't create me!!
91        throw new UnsupportedOperationException();
92    }
93
94    /**
95     * ECMA 15.12.2 parse ( text [ , reviver ] )
96     *
97     * @param self     self reference
98     * @param text     a JSON formatted string
99     * @param reviver  optional value: function that takes two parameters (key, value)
100     *
101     * @return an ECMA script value
102     */
103    @Function(attributes = Attribute.NOT_ENUMERABLE, where = Where.CONSTRUCTOR)
104    public static Object parse(final Object self, final Object text, final Object reviver) {
105        return JSONFunctions.parse(text, reviver);
106    }
107
108    /**
109     * ECMA 15.12.3 stringify ( value [ , replacer [ , space ] ] )
110     *
111     * @param self     self reference
112     * @param value    ECMA script value (usually object or array)
113     * @param replacer either a function or an array of strings and numbers
114     * @param space    optional parameter - allows result to have whitespace injection
115     *
116     * @return a string in JSON format
117     */
118    @Function(attributes = Attribute.NOT_ENUMERABLE, where = Where.CONSTRUCTOR)
119    public static Object stringify(final Object self, final Object value, final Object replacer, final Object space) {
120        // The stringify method takes a value and an optional replacer, and an optional
121        // space parameter, and returns a JSON text. The replacer can be a function
122        // that can replace values, or an array of strings that will select the keys.
123
124        // A default replacer method can be provided. Use of the space parameter can
125        // produce text that is more easily readable.
126
127        final StringifyState state = new StringifyState();
128
129        // If there is a replacer, it must be a function or an array.
130        if (replacer instanceof ScriptFunction) {
131            state.replacerFunction = (ScriptFunction) replacer;
132        } else if (isArray(replacer) ||
133                replacer instanceof Iterable ||
134                (replacer != null && replacer.getClass().isArray())) {
135
136            state.propertyList = new ArrayList<>();
137
138            final Iterator<Object> iter = ArrayLikeIterator.arrayLikeIterator(replacer);
139
140            while (iter.hasNext()) {
141                String item = null;
142                final Object v = iter.next();
143
144                if (v instanceof String) {
145                    item = (String) v;
146                } else if (v instanceof ConsString) {
147                    item = v.toString();
148                } else if (v instanceof Number ||
149                        v instanceof NativeNumber ||
150                        v instanceof NativeString) {
151                    item = JSType.toString(v);
152                }
153
154                if (item != null) {
155                    state.propertyList.add(item);
156                }
157            }
158        }
159
160        // If the space parameter is a number, make an indent
161        // string containing that many spaces.
162
163        String gap;
164
165        // modifiable 'space' - parameter is final
166        Object modSpace = space;
167        if (modSpace instanceof NativeNumber) {
168            modSpace = JSType.toNumber(JSType.toPrimitive(modSpace, Number.class));
169        } else if (modSpace instanceof NativeString) {
170            modSpace = JSType.toString(JSType.toPrimitive(modSpace, String.class));
171        }
172
173        if (modSpace instanceof Number) {
174            final int indent = Math.min(10, JSType.toInteger(modSpace));
175            if (indent < 1) {
176                gap = "";
177            } else {
178                final StringBuilder sb = new StringBuilder();
179                for (int i = 0; i < indent; i++) {
180                    sb.append(' ');
181                }
182                gap = sb.toString();
183            }
184        } else if (modSpace instanceof String || modSpace instanceof ConsString) {
185            final String str = modSpace.toString();
186            gap = str.substring(0, Math.min(10, str.length()));
187        } else {
188            gap = "";
189        }
190
191        state.gap = gap;
192
193        final ScriptObject wrapper = Global.newEmptyInstance();
194        wrapper.set("", value, 0);
195
196        return str("", wrapper, state);
197    }
198
199    // -- Internals only below this point
200
201    // stringify helpers.
202
203    private static class StringifyState {
204        final Map<ScriptObject, ScriptObject> stack = new IdentityHashMap<>();
205
206        StringBuilder  indent = new StringBuilder();
207        String         gap = "";
208        List<String>   propertyList = null;
209        ScriptFunction replacerFunction = null;
210    }
211
212    // Spec: The abstract operation Str(key, holder).
213    private static Object str(final Object key, final ScriptObject holder, final StringifyState state) {
214        Object value = holder.get(key);
215
216        try {
217            if (value instanceof ScriptObject) {
218                final InvokeByName toJSONInvoker = getTO_JSON();
219                final ScriptObject svalue = (ScriptObject)value;
220                final Object toJSON = toJSONInvoker.getGetter().invokeExact(svalue);
221                if (Bootstrap.isCallable(toJSON)) {
222                    value = toJSONInvoker.getInvoker().invokeExact(toJSON, svalue, key);
223                }
224            }
225
226            if (state.replacerFunction != null) {
227                value = getREPLACER_INVOKER().invokeExact(state.replacerFunction, holder, key, value);
228            }
229        } catch(Error|RuntimeException t) {
230            throw t;
231        } catch(final Throwable t) {
232            throw new RuntimeException(t);
233        }
234        final boolean isObj = (value instanceof ScriptObject);
235        if (isObj) {
236            if (value instanceof NativeNumber) {
237                value = JSType.toNumber(value);
238            } else if (value instanceof NativeString) {
239                value = JSType.toString(value);
240            } else if (value instanceof NativeBoolean) {
241                value = ((NativeBoolean)value).booleanValue();
242            }
243        }
244
245        if (value == null) {
246            return "null";
247        } else if (Boolean.TRUE.equals(value)) {
248            return "true";
249        } else if (Boolean.FALSE.equals(value)) {
250            return "false";
251        }
252
253        if (value instanceof String) {
254            return JSONFunctions.quote((String)value);
255        } else if (value instanceof ConsString) {
256            return JSONFunctions.quote(value.toString());
257        }
258
259        if (value instanceof Number) {
260            return JSType.isFinite(((Number)value).doubleValue()) ? JSType.toString(value) : "null";
261        }
262
263        final JSType type = JSType.of(value);
264        if (type == JSType.OBJECT) {
265            if (isArray(value)) {
266                return JA((ScriptObject)value, state);
267            } else if (value instanceof ScriptObject) {
268                return JO((ScriptObject)value, state);
269            }
270        }
271
272        return UNDEFINED;
273    }
274
275    // Spec: The abstract operation JO(value) serializes an object.
276    private static String JO(final ScriptObject value, final StringifyState state) {
277        if (state.stack.containsKey(value)) {
278            throw typeError("JSON.stringify.cyclic");
279        }
280
281        state.stack.put(value, value);
282        final StringBuilder stepback = new StringBuilder(state.indent.toString());
283        state.indent.append(state.gap);
284
285        final StringBuilder finalStr = new StringBuilder();
286        final List<Object>  partial  = new ArrayList<>();
287        final List<String>  k        = state.propertyList == null ? Arrays.asList(value.getOwnKeys(false)) : state.propertyList;
288
289        for (final Object p : k) {
290            final Object strP = str(p, value, state);
291
292            if (strP != UNDEFINED) {
293                final StringBuilder member = new StringBuilder();
294
295                member.append(JSONFunctions.quote(p.toString())).append(':');
296                if (!state.gap.isEmpty()) {
297                    member.append(' ');
298                }
299
300                member.append(strP);
301                partial.add(member);
302            }
303        }
304
305        if (partial.isEmpty()) {
306            finalStr.append("{}");
307        } else {
308            if (state.gap.isEmpty()) {
309                final int size = partial.size();
310                int       index = 0;
311
312                finalStr.append('{');
313
314                for (final Object str : partial) {
315                    finalStr.append(str);
316                    if (index < size - 1) {
317                        finalStr.append(',');
318                    }
319                    index++;
320                }
321
322                finalStr.append('}');
323            } else {
324                final int size  = partial.size();
325                int       index = 0;
326
327                finalStr.append("{\n");
328                finalStr.append(state.indent);
329
330                for (final Object str : partial) {
331                    finalStr.append(str);
332                    if (index < size - 1) {
333                        finalStr.append(",\n");
334                        finalStr.append(state.indent);
335                    }
336                    index++;
337                }
338
339                finalStr.append('\n');
340                finalStr.append(stepback);
341                finalStr.append('}');
342            }
343        }
344
345        state.stack.remove(value);
346        state.indent = stepback;
347
348        return finalStr.toString();
349    }
350
351    // Spec: The abstract operation JA(value) serializes an array.
352    private static Object JA(final ScriptObject value, final StringifyState state) {
353        if (state.stack.containsKey(value)) {
354            throw typeError("JSON.stringify.cyclic");
355        }
356
357        state.stack.put(value, value);
358        final StringBuilder stepback = new StringBuilder(state.indent.toString());
359        state.indent.append(state.gap);
360        final List<Object> partial = new ArrayList<>();
361
362        final int length = JSType.toInteger(value.getLength());
363        int index = 0;
364
365        while (index < length) {
366            Object strP = str(index, value, state);
367            if (strP == UNDEFINED) {
368                strP = "null";
369            }
370            partial.add(strP);
371            index++;
372        }
373
374        final StringBuilder finalStr = new StringBuilder();
375        if (partial.isEmpty()) {
376            finalStr.append("[]");
377        } else {
378            if (state.gap.isEmpty()) {
379                final int size = partial.size();
380                index = 0;
381                finalStr.append('[');
382                for (final Object str : partial) {
383                    finalStr.append(str);
384                    if (index < size - 1) {
385                        finalStr.append(',');
386                    }
387                    index++;
388                }
389
390                finalStr.append(']');
391            } else {
392                final int size = partial.size();
393                index = 0;
394                finalStr.append("[\n");
395                finalStr.append(state.indent);
396                for (final Object str : partial) {
397                    finalStr.append(str);
398                    if (index < size - 1) {
399                        finalStr.append(",\n");
400                        finalStr.append(state.indent);
401                    }
402                    index++;
403                }
404
405                finalStr.append('\n');
406                finalStr.append(stepback);
407                finalStr.append(']');
408            }
409        }
410
411        state.stack.remove(value);
412        state.indent = stepback;
413
414        return finalStr.toString();
415    }
416}
417