1 /* 2 * Copyright (c) 2025, 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 oracle.code.json; 27 28 import java.util.Collections; 29 import java.util.HashMap; 30 import java.util.Map; 31 import java.util.Objects; 32 import java.util.Set; 33 34 /** 35 * JsonObject implementation class 36 */ 37 final class JsonObjectImpl implements JsonObject, JsonValueImpl { 38 39 private final JsonDocumentInfo docInfo; 40 private final int startIndex; 41 private final int endIndex; 42 private Map<String, JsonValue> theKeys; 43 44 // Via of factory 45 JsonObjectImpl(Map<String, ? extends JsonValue> map) { 46 theKeys = Collections.unmodifiableMap(map); 47 docInfo = null; 48 startIndex = 0; 49 endIndex = 0; 50 } 51 52 // Via untyped 53 JsonObjectImpl(Map<?, ?> map, Set<Object> untypedObjs, int depth) { 54 HashMap<String, JsonValue> m = HashMap.newHashMap(map.size()); 55 for (Map.Entry<?, ?> entry : map.entrySet()) { 56 if (!(entry.getKey() instanceof String strKey)) { 57 throw new IllegalArgumentException("Key is not a String: " + entry.getKey()); 58 } else { 59 m.put(strKey, JsonGenerator.fromUntyped(entry.getValue(), untypedObjs, depth)); 60 } 61 } 62 theKeys = Collections.unmodifiableMap(m); 63 docInfo = null; 64 startIndex = 0; 65 endIndex = 0; 66 } 67 68 JsonObjectImpl(JsonDocumentInfo doc, int index) { 69 docInfo = doc; 70 startIndex = index; 71 endIndex = startIndex == 0 ? docInfo.getIndexCount() - 1 // For root 72 : docInfo.nextIndex(index, '{', '}'); 73 } 74 75 @Override 76 public Map<String, JsonValue> keys() { 77 if (theKeys == null) { 78 theKeys = inflate(); 79 } 80 return theKeys; 81 } 82 83 // Inflates the JsonObject using the tokens array 84 private Map<String, JsonValue> inflate() { 85 var k = new HashMap<String, JsonValue>(); 86 var index = startIndex + 1; 87 // Empty case automatically checked by index increment. {} is 2 tokens 88 while (index < endIndex) { 89 // Member name should be source string, not unescaped 90 // Member equality is done via unescaped in JsonParser 91 var key = docInfo.substring( 92 docInfo.getOffset(index) + 1, docInfo.getOffset(index + 1)); 93 index = index + 2; 94 95 // Get value 96 int offset = docInfo.getOffset(index) + 1; 97 if (docInfo.shouldWalkToken(docInfo.charAtIndex(index + 1))) { 98 index++; 99 } 100 var value = JsonGenerator.createValue(docInfo, offset, index); 101 102 // Store key and value 103 k.put(key, value); 104 // Move to the next key 105 index = ((JsonValueImpl)value).getEndIndex() + 1; 106 } 107 return Collections.unmodifiableMap(k); 108 } 109 110 @Override 111 public int getEndIndex() { 112 return endIndex + 1; // We are interested in the index after '}' 113 } 114 115 @Override 116 public boolean equals(Object o) { 117 return this == o || 118 o instanceof JsonObjectImpl ojoi && 119 Objects.equals(keys(), ojoi.keys()); 120 } 121 122 @Override 123 public int hashCode() { 124 return Objects.hash(keys()); 125 } 126 127 @Override 128 public Map<String, Object> toUntyped() { 129 return keys().entrySet().stream() 130 .collect(HashMap::new, // to allow `null` value 131 (m, e) -> m.put(e.getKey(), Json.toUntyped(e.getValue())), 132 HashMap::putAll); 133 } 134 135 @Override 136 public String toString() { 137 var s = new StringBuilder("{"); 138 for (Map.Entry<String, JsonValue> kv: keys().entrySet()) { 139 s.append("\"").append(kv.getKey()).append("\":") 140 .append(kv.getValue().toString()) 141 .append(","); 142 } 143 if (!keys().isEmpty()) { 144 s.setLength(s.length() - 1); // trim final comma 145 } 146 return s.append("}").toString(); 147 } 148 149 @Override 150 public String toDisplayString(int indent, boolean isField) { 151 var prefix = " ".repeat(indent); 152 var s = new StringBuilder(isField ? " " : prefix); 153 if (keys().isEmpty()) { 154 s.append("{}"); 155 } else { 156 s.append("{\n"); 157 keys().entrySet().stream() 158 .sorted(Map.Entry.comparingByKey(String::compareTo)) 159 .forEach(e -> { 160 var key = e.getKey(); 161 var value = e.getValue(); 162 if (value instanceof JsonValueImpl val) { 163 s.append(prefix) 164 .append(" ".repeat(INDENT)) 165 .append("\"") 166 .append(key) 167 .append("\":") 168 .append(val.toDisplayString(indent + INDENT, true)) 169 .append(",\n"); 170 } else { 171 throw new InternalError("type mismatch"); 172 } 173 }); 174 s.setLength(s.length() - 2); // trim final comma 175 s.append("\n").append(prefix).append("}"); 176 } 177 return s.toString(); 178 } 179 }