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 java.util.Objects;
  39 import java.util.concurrent.Callable;
  40 import jdk.nashorn.api.scripting.JSObject;
  41 import jdk.nashorn.api.scripting.ScriptObjectMirror;
  42 import jdk.nashorn.internal.objects.annotations.Attribute;
  43 import jdk.nashorn.internal.objects.annotations.Function;
  44 import jdk.nashorn.internal.objects.annotations.ScriptClass;
  45 import jdk.nashorn.internal.objects.annotations.Where;
  46 import jdk.nashorn.internal.runtime.ConsString;
  47 import jdk.nashorn.internal.runtime.JSONFunctions;
  48 import jdk.nashorn.internal.runtime.JSType;
  49 import jdk.nashorn.internal.runtime.PropertyMap;
  50 import jdk.nashorn.internal.runtime.ScriptObject;
  51 import jdk.nashorn.internal.runtime.arrays.ArrayLikeIterator;
  52 import jdk.nashorn.internal.runtime.linker.Bootstrap;
  53 import jdk.nashorn.internal.runtime.linker.InvokeByName;
  54 
  55 /**
  56  * ECMAScript 262 Edition 5, Section 15.12 The NativeJSON Object
  57  *
  58  */
  59 @ScriptClass("JSON")
  60 public 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 }