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 jdk.nashorn.internal.objects.annotations.Attribute; 39 import jdk.nashorn.internal.objects.annotations.Function; 40 import jdk.nashorn.internal.objects.annotations.ScriptClass; 41 import jdk.nashorn.internal.objects.annotations.Where; 42 import jdk.nashorn.internal.runtime.ConsString; 43 import jdk.nashorn.internal.runtime.JSONFunctions; 44 import jdk.nashorn.internal.runtime.JSType; 45 import jdk.nashorn.internal.runtime.ScriptFunction; 46 import jdk.nashorn.internal.runtime.ScriptObject; 47 import jdk.nashorn.internal.runtime.arrays.ArrayLikeIterator; 48 import jdk.nashorn.internal.runtime.linker.Bootstrap; 49 import jdk.nashorn.internal.runtime.linker.InvokeByName; 50 51 /** 52 * ECMAScript 262 Edition 5, Section 15.12 The NativeJSON Object 53 * 54 */ 55 @ScriptClass("JSON") 56 public final class NativeJSON extends ScriptObject { 57 private static final InvokeByName TO_JSON = new InvokeByName("toJSON", ScriptObject.class, Object.class, Object.class); 58 private static final MethodHandle REPLACER_INVOKER = Bootstrap.createDynamicInvoker("dyn:call", Object.class, 59 ScriptFunction.class, ScriptObject.class, Object.class, Object.class); 60 61 62 NativeJSON() { 63 this.setProto(Global.objectPrototype()); 64 } 65 66 /** 67 * ECMA 15.12.2 parse ( text [ , reviver ] ) 68 * 69 * @param self self reference 70 * @param text a JSON formatted string 71 * @param reviver optional value: function that takes two parameters (key, value) 72 * 73 * @return an ECMA script value 74 */ 75 @Function(attributes = Attribute.NOT_ENUMERABLE, where = Where.CONSTRUCTOR) 76 public static Object parse(final Object self, final Object text, final Object reviver) { 77 return JSONFunctions.parse(text, reviver); 78 } 79 80 /** 81 * ECMA 15.12.3 stringify ( value [ , replacer [ , space ] ] ) 82 * 83 * @param self self reference 84 * @param value ECMA script value (usually object or array) 85 * @param replacer either a function or an array of strings and numbers 86 * @param space optional parameter - allows result to have whitespace injection 87 * 88 * @return a string in JSON format 89 */ 90 @Function(attributes = Attribute.NOT_ENUMERABLE, where = Where.CONSTRUCTOR) 91 public static Object stringify(final Object self, final Object value, final Object replacer, final Object space) { 92 // The stringify method takes a value and an optional replacer, and an optional 93 // space parameter, and returns a JSON text. The replacer can be a function 94 // that can replace values, or an array of strings that will select the keys. 95 96 // A default replacer method can be provided. Use of the space parameter can 97 // produce text that is more easily readable. 98 99 final StringifyState state = new StringifyState(); 100 101 // If there is a replacer, it must be a function or an array. 102 if (replacer instanceof ScriptFunction) { 103 state.replacerFunction = (ScriptFunction) replacer; 104 } else if (isArray(replacer) || 105 replacer instanceof Iterable || 106 (replacer != null && replacer.getClass().isArray())) { 107 108 state.propertyList = new ArrayList<>(); 109 110 final Iterator<Object> iter = ArrayLikeIterator.arrayLikeIterator(replacer); 111 112 while (iter.hasNext()) { 113 String item = null; 114 final Object v = iter.next(); 115 116 if (v instanceof String) { 117 item = (String) v; 118 } else if (v instanceof ConsString) { 119 item = v.toString(); 120 } else if (v instanceof Number || 121 v instanceof NativeNumber || 122 v instanceof NativeString) { 123 item = JSType.toString(v); 124 } 125 126 if (item != null) { 127 state.propertyList.add(item); 128 } 129 } 130 } 131 132 // If the space parameter is a number, make an indent 133 // string containing that many spaces. 134 135 String gap; 136 137 if (space instanceof Number || space instanceof NativeNumber) { 138 int indent; 139 if (space instanceof NativeNumber) { 140 indent = ((NativeNumber)space).intValue(); 141 } else { 142 indent = ((Number)space).intValue(); 143 } 144 145 final StringBuilder sb = new StringBuilder(); 146 for (int i = 0; i < Math.min(10, indent); i++) { 147 sb.append(' '); 148 } 149 gap = sb.toString(); 150 151 } else if (space instanceof String || space instanceof ConsString || space instanceof NativeString) { 152 final String str = (space instanceof String) ? (String)space : space.toString(); 153 gap = str.substring(0, Math.min(10, str.length())); 154 } else { 155 gap = ""; 156 } 157 158 state.gap = gap; 159 160 final ScriptObject wrapper = Global.newEmptyInstance(); 161 wrapper.set("", value, Global.isStrict()); 162 163 return str("", wrapper, state); 164 } 165 166 // -- Internals only below this point 167 168 // stringify helpers. 169 170 private static class StringifyState { 171 final Map<ScriptObject, ScriptObject> stack = new IdentityHashMap<>(); 172 173 StringBuilder indent = new StringBuilder(); 174 String gap = ""; 175 List<String> propertyList = null; 176 ScriptFunction replacerFunction = null; 177 } 178 179 // Spec: The abstract operation Str(key, holder). 180 private static Object str(final Object key, final ScriptObject holder, final StringifyState state) { 181 Object value = holder.get(key); 182 183 try { 184 if (value instanceof ScriptObject) { 185 final ScriptObject svalue = (ScriptObject)value; 186 final Object toJSON = TO_JSON.getGetter().invokeExact(svalue); 187 if (toJSON instanceof ScriptFunction) { 188 value = TO_JSON.getInvoker().invokeExact(toJSON, svalue, key); 189 } 190 } 191 192 if (state.replacerFunction != null) { 193 value = REPLACER_INVOKER.invokeExact(state.replacerFunction, holder, key, value); 194 } 195 } catch(Error|RuntimeException t) { 196 throw t; 197 } catch(final Throwable t) { 198 throw new RuntimeException(t); 199 } 200 final boolean isObj = (value instanceof ScriptObject); 201 if (isObj) { 202 if (value instanceof NativeNumber) { 203 value = JSType.toNumber(value); 204 } else if (value instanceof NativeString) { 205 value = JSType.toString(value); 206 } else if (value instanceof NativeBoolean) { 207 value = ((NativeBoolean)value).booleanValue(); 208 } 209 } 210 211 if (value == null) { 212 return "null"; 213 } else if (Boolean.TRUE.equals(value)) { 214 return "true"; 215 } else if (Boolean.FALSE.equals(value)) { 216 return "false"; 217 } 218 219 if (value instanceof String) { 220 return JSONFunctions.quote((String)value); 221 } else if (value instanceof ConsString) { 222 return JSONFunctions.quote(value.toString()); 223 } 224 225 if (value instanceof Number) { 226 return JSType.isFinite(((Number)value).doubleValue()) ? JSType.toString(value) : "null"; 227 } 228 229 final JSType type = JSType.of(value); 230 if (type == JSType.OBJECT) { 231 if (isArray(value)) { 232 return JA((ScriptObject)value, state); 233 } else if (value instanceof ScriptObject) { 234 return JO((ScriptObject)value, state); 235 } 236 } 237 238 return UNDEFINED; 239 } 240 241 // Spec: The abstract operation JO(value) serializes an object. 242 private static String JO(final ScriptObject value, final StringifyState state) { 243 if (state.stack.containsKey(value)) { 244 throw typeError("JSON.stringify.cyclic"); 245 } 246 247 state.stack.put(value, value); 248 final StringBuilder stepback = new StringBuilder(state.indent.toString()); 249 state.indent.append(state.gap); 250 251 final StringBuilder finalStr = new StringBuilder(); 252 final List<Object> partial = new ArrayList<>(); 253 final List<String> k = state.propertyList == null ? Arrays.asList(value.getOwnKeys(false)) : state.propertyList; 254 255 for (final Object p : k) { 256 final Object strP = str(p, value, state); 257 258 if (strP != UNDEFINED) { 259 final StringBuilder member = new StringBuilder(); 260 261 member.append(JSONFunctions.quote(p.toString())).append(':'); 262 if (!state.gap.isEmpty()) { 263 member.append(' '); 264 } 265 266 member.append(strP); 267 partial.add(member); 268 } 269 } 270 271 if (partial.isEmpty()) { 272 finalStr.append("{}"); 273 } else { 274 if (state.gap.isEmpty()) { 275 final int size = partial.size(); 276 int index = 0; 277 278 finalStr.append('{'); 279 280 for (final Object str : partial) { 281 finalStr.append(str); 282 if (index < size - 1) { 283 finalStr.append(','); 284 } 285 index++; 286 } 287 288 finalStr.append('}'); 289 } else { 290 final int size = partial.size(); 291 int index = 0; 292 293 finalStr.append("{\n"); 294 finalStr.append(state.indent); 295 296 for (final Object str : partial) { 297 finalStr.append(str); 298 if (index < size - 1) { 299 finalStr.append(",\n"); 300 finalStr.append(state.indent); 301 } 302 index++; 303 } 304 305 finalStr.append('\n'); 306 finalStr.append(stepback); 307 finalStr.append('}'); 308 } 309 } 310 311 state.stack.remove(value); 312 state.indent = stepback; 313 314 return finalStr.toString(); 315 } 316 317 // Spec: The abstract operation JA(value) serializes an array. 318 private static Object JA(final ScriptObject value, final StringifyState state) { 319 if (state.stack.containsKey(value)) { 320 throw typeError("JSON.stringify.cyclic"); 321 } 322 323 state.stack.put(value, value); 324 final StringBuilder stepback = new StringBuilder(state.indent.toString()); 325 state.indent.append(state.gap); 326 final List<Object> partial = new ArrayList<>(); 327 328 final int length = JSType.toInteger(value.getLength()); 329 int index = 0; 330 331 while (index < length) { 332 Object strP = str(index, value, state); 333 if (strP == UNDEFINED) { 334 strP = "null"; 335 } 336 partial.add(strP); 337 index++; 338 } 339 340 final StringBuilder finalStr = new StringBuilder(); 341 if (partial.isEmpty()) { 342 finalStr.append("[]"); 343 } else { 344 if (state.gap.isEmpty()) { 345 final int size = partial.size(); 346 index = 0; 347 finalStr.append('['); 348 for (final Object str : partial) { 349 finalStr.append(str); 350 if (index < size - 1) { 351 finalStr.append(','); 352 } 353 index++; 354 } 355 356 finalStr.append(']'); 357 } else { 358 final int size = partial.size(); 359 index = 0; 360 finalStr.append("[\n"); 361 finalStr.append(state.indent); 362 for (final Object str : partial) { 363 finalStr.append(str); 364 if (index < size - 1) { 365 finalStr.append(",\n"); 366 finalStr.append(state.indent); 367 } 368 index++; 369 } 370 371 finalStr.append('\n'); 372 finalStr.append(stepback); 373 finalStr.append(']'); 374 } 375 } 376 377 state.stack.remove(value); 378 state.indent = stepback; 379 380 return finalStr.toString(); 381 } 382 }