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