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