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