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 }