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 }