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         if (space instanceof Number || space instanceof NativeNumber) {
 166             int indent;
 167             if (space instanceof NativeNumber) {
 168                 indent = ((NativeNumber)space).intValue();
 169             } else {
 170                 indent = ((Number)space).intValue();
 171             }
 172 
 173             final StringBuilder sb = new StringBuilder();
 174             for (int i = 0; i < Math.min(10, indent); i++) {
 175                 sb.append(' ');
 176             }
 177             gap = sb.toString();
 178 
 179         } else if (space instanceof String || space instanceof ConsString || space instanceof NativeString) {
 180             final String str = (space instanceof String) ? (String)space : space.toString();
 181             gap = str.substring(0, Math.min(10, str.length()));
 182         } else {
 183             gap = "";
 184         }
 185 
 186         state.gap = gap;
 187 
 188         final ScriptObject wrapper = Global.newEmptyInstance();
 189         wrapper.set("", value, false);
 190 
 191         return str("", wrapper, state);
 192     }
 193 
 194     // -- Internals only below this point
 195 
 196     // stringify helpers.
 197 
 198     private static class StringifyState {
 199         final Map<ScriptObject, ScriptObject> stack = new IdentityHashMap<>();
 200 
 201         StringBuilder  indent = new StringBuilder();
 202         String         gap = "";
 203         List<String>   propertyList = null;
 204         ScriptFunction replacerFunction = null;
 205     }
 206 
 207     // Spec: The abstract operation Str(key, holder).
 208     private static Object str(final Object key, final ScriptObject holder, final StringifyState state) {
 209         Object value = holder.get(key);
 210 
 211         try {
 212             if (value instanceof ScriptObject) {
 213                 final InvokeByName toJSONInvoker = getTO_JSON();
 214                 final ScriptObject svalue = (ScriptObject)value;
 215                 final Object toJSON = toJSONInvoker.getGetter().invokeExact(svalue);
 216                 if (Bootstrap.isCallable(toJSON)) {
 217                     value = toJSONInvoker.getInvoker().invokeExact(toJSON, svalue, key);
 218                 }
 219             }
 220 
 221             if (state.replacerFunction != null) {
 222                 value = getREPLACER_INVOKER().invokeExact(state.replacerFunction, holder, key, value);
 223             }
 224         } catch(Error|RuntimeException t) {
 225             throw t;
 226         } catch(final Throwable t) {
 227             throw new RuntimeException(t);
 228         }
 229         final boolean isObj = (value instanceof ScriptObject);
 230         if (isObj) {
 231             if (value instanceof NativeNumber) {
 232                 value = JSType.toNumber(value);
 233             } else if (value instanceof NativeString) {
 234                 value = JSType.toString(value);
 235             } else if (value instanceof NativeBoolean) {
 236                 value = ((NativeBoolean)value).booleanValue();
 237             }
 238         }
 239 
 240         if (value == null) {
 241             return "null";
 242         } else if (Boolean.TRUE.equals(value)) {
 243             return "true";
 244         } else if (Boolean.FALSE.equals(value)) {
 245             return "false";
 246         }
 247 
 248         if (value instanceof String) {
 249             return JSONFunctions.quote((String)value);
 250         } else if (value instanceof ConsString) {
 251             return JSONFunctions.quote(value.toString());
 252         }
 253 
 254         if (value instanceof Number) {
 255             return JSType.isFinite(((Number)value).doubleValue()) ? JSType.toString(value) : "null";
 256         }
 257 
 258         final JSType type = JSType.of(value);
 259         if (type == JSType.OBJECT) {
 260             if (isArray(value)) {
 261                 return JA((ScriptObject)value, state);
 262             } else if (value instanceof ScriptObject) {
 263                 return JO((ScriptObject)value, state);
 264             }
 265         }
 266 
 267         return UNDEFINED;
 268     }
 269 
 270     // Spec: The abstract operation JO(value) serializes an object.
 271     private static String JO(final ScriptObject value, final StringifyState state) {
 272         if (state.stack.containsKey(value)) {
 273             throw typeError("JSON.stringify.cyclic");
 274         }
 275 
 276         state.stack.put(value, value);
 277         final StringBuilder stepback = new StringBuilder(state.indent.toString());
 278         state.indent.append(state.gap);
 279 
 280         final StringBuilder finalStr = new StringBuilder();
 281         final List<Object>  partial  = new ArrayList<>();
 282         final List<String>  k        = state.propertyList == null ? Arrays.asList(value.getOwnKeys(false)) : state.propertyList;
 283 
 284         for (final Object p : k) {
 285             final Object strP = str(p, value, state);
 286 
 287             if (strP != UNDEFINED) {
 288                 final StringBuilder member = new StringBuilder();
 289 
 290                 member.append(JSONFunctions.quote(p.toString())).append(':');
 291                 if (!state.gap.isEmpty()) {
 292                     member.append(' ');
 293                 }
 294 
 295                 member.append(strP);
 296                 partial.add(member);
 297             }
 298         }
 299 
 300         if (partial.isEmpty()) {
 301             finalStr.append("{}");
 302         } else {
 303             if (state.gap.isEmpty()) {
 304                 final int size = partial.size();
 305                 int       index = 0;
 306 
 307                 finalStr.append('{');
 308 
 309                 for (final Object str : partial) {
 310                     finalStr.append(str);
 311                     if (index < size - 1) {
 312                         finalStr.append(',');
 313                     }
 314                     index++;
 315                 }
 316 
 317                 finalStr.append('}');
 318             } else {
 319                 final int size  = partial.size();
 320                 int       index = 0;
 321 
 322                 finalStr.append("{\n");
 323                 finalStr.append(state.indent);
 324 
 325                 for (final Object str : partial) {
 326                     finalStr.append(str);
 327                     if (index < size - 1) {
 328                         finalStr.append(",\n");
 329                         finalStr.append(state.indent);
 330                     }
 331                     index++;
 332                 }
 333 
 334                 finalStr.append('\n');
 335                 finalStr.append(stepback);
 336                 finalStr.append('}');
 337             }
 338         }
 339 
 340         state.stack.remove(value);
 341         state.indent = stepback;
 342 
 343         return finalStr.toString();
 344     }
 345 
 346     // Spec: The abstract operation JA(value) serializes an array.
 347     private static Object JA(final ScriptObject value, final StringifyState state) {
 348         if (state.stack.containsKey(value)) {
 349             throw typeError("JSON.stringify.cyclic");
 350         }
 351 
 352         state.stack.put(value, value);
 353         final StringBuilder stepback = new StringBuilder(state.indent.toString());
 354         state.indent.append(state.gap);
 355         final List<Object> partial = new ArrayList<>();
 356 
 357         final int length = JSType.toInteger(value.getLength());
 358         int index = 0;
 359 
 360         while (index < length) {
 361             Object strP = str(index, value, state);
 362             if (strP == UNDEFINED) {
 363                 strP = "null";
 364             }
 365             partial.add(strP);
 366             index++;
 367         }
 368 
 369         final StringBuilder finalStr = new StringBuilder();
 370         if (partial.isEmpty()) {
 371             finalStr.append("[]");
 372         } else {
 373             if (state.gap.isEmpty()) {
 374                 final int size = partial.size();
 375                 index = 0;
 376                 finalStr.append('[');
 377                 for (final Object str : partial) {
 378                     finalStr.append(str);
 379                     if (index < size - 1) {
 380                         finalStr.append(',');
 381                     }
 382                     index++;
 383                 }
 384 
 385                 finalStr.append(']');
 386             } else {
 387                 final int size = partial.size();
 388                 index = 0;
 389                 finalStr.append("[\n");
 390                 finalStr.append(state.indent);
 391                 for (final Object str : partial) {
 392                     finalStr.append(str);
 393                     if (index < size - 1) {
 394                         finalStr.append(",\n");
 395                         finalStr.append(state.indent);
 396                     }
 397                     index++;
 398                 }
 399 
 400                 finalStr.append('\n');
 401                 finalStr.append(stepback);
 402                 finalStr.append(']');
 403             }
 404         }
 405 
 406         state.stack.remove(value);
 407         state.indent = stepback;
 408 
 409         return finalStr.toString();
 410     }
 411 }