001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.marshall.collections; 018import static org.apache.juneau.commons.utils.Shorts.*; 019 020import java.util.*; 021 022import org.apache.juneau.marshall.*; 023 024/** 025 * Typed tree façade over the generic-collections model ({@link MarshalledMap} / {@link MarshalledList} and their 026 * {@link JsonMap} / {@link JsonList} subclasses). 027 * 028 * <p> 029 * This is a <b>live, mutating view</b> (a façade — <i>not</i> a parallel node hierarchy) wrapping a single backing 030 * value which is one of: 031 * <ul> 032 * <li>a {@link Map} (an <i>object</i> node), 033 * <li>a {@link List} (an <i>array</i> node), 034 * <li>a scalar value ({@link String}, {@link Number}, {@link Boolean}, etc.) (a <i>value</i> node), or 035 * <li><jk>null</jk> (a <i>null</i> node). 036 * </ul> 037 * 038 * <p> 039 * It gives Jackson-migrants the ergonomics they expect (node-type introspection, typed value accessors, and fluent 040 * tree building) without replacing the underlying {@link Map}/{@link List} representation. Because it is a live view, 041 * mutating builders such as {@link #put(String,Object)} and {@link #add(Object)} mutate the backing container 042 * <i>in place</i> — callers holding the underlying map/list see the change. Use {@link #copy()} for an independent 043 * deep-copy snapshot. 044 * 045 * <h5 class='section'>Example:</h5> 046 * <p class='bjava'> 047 * <jc>// Build a tree fluently (mutates a backing JsonMap/JsonList in place).</jc> 048 * MarshalledNode <jv>node</jv> = MarshalledNode.<jsm>objectNode</jsm>() 049 * .put(<js>"a"</js>, 1) 050 * .put(<js>"b"</js>, MarshalledNode.<jsm>arrayNode</jsm>().add(<js>"x"</js>).add(<js>"y"</js>).value()); 051 * 052 * String <jv>json</jv> = Json.<jsm>of</jsm>(<jv>node</jv>.value()); <jc>// {"a":1,"b":["x","y"]}</jc> 053 * 054 * <jc>// Navigate and read typed values.</jc> 055 * Integer <jv>a</jv> = <jv>node</jv>.get(<js>"a"</js>).asInt(); <jc>// 1</jc> 056 * String <jv>x</jv> = <jv>node</jv>.get(<js>"b"</js>).get(0).asString(); <jc>// "x"</jc> 057 * </p> 058 * 059 * <h5 class='section'>Notes:</h5><ul> 060 * <li class='warn'>This class is not thread safe. 061 * </ul> 062 * 063 * <p> 064 * <b>Beta — API subject to change:</b> This type is part of the next-generation typed tree model layered over the 065 * {@code Marshalled*} collections and may change incompatibly in a future release. 066 */ 067@SuppressWarnings({ 068 "unchecked" // put()/add() cast the node value to Map<String,Object>/List<Object> only after checking the node is an object/array node 069}) 070public class MarshalledNode { 071 072 /** 073 * Wraps any value as a node. 074 * 075 * <p> 076 * The value is stored as-is (no copy): a {@link Map} becomes an object node, a {@link List} becomes an array node, 077 * a non-<jk>null</jk> non-container value becomes a value node, and <jk>null</jk> becomes a null node. For navigation 078 * and fluent building to work the value should be (or contain) the {@link MarshalledMap}/{@link MarshalledList} 079 * family, but any {@link Map}/{@link List} is accepted. 080 * 081 * @param value The value to wrap. Can be <jk>null</jk> (creates a <i>null</i> node). 082 * @return A new node wrapping the value, never <jk>null</jk>. 083 */ 084 public static MarshalledNode of(Object value) { 085 return new MarshalledNode(value); 086 } 087 088 /** 089 * Creates a new object node backed by a new empty {@link JsonMap}. 090 * 091 * @return A new object node, never <jk>null</jk>. 092 */ 093 public static MarshalledNode objectNode() { 094 return new MarshalledNode(new JsonMap()); 095 } 096 097 /** 098 * Creates a new array node backed by a new empty {@link JsonList}. 099 * 100 * @return A new array node, never <jk>null</jk>. 101 */ 102 public static MarshalledNode arrayNode() { 103 return new MarshalledNode(new JsonList()); 104 } 105 106 private final Object value; 107 108 private MarshalledNode(Object value) { 109 this.value = value; 110 } 111 112 /** 113 * Returns <jk>true</jk> if this node wraps a {@link Map} (an object node). 114 * 115 * @return <jk>true</jk> if this is an object node. 116 */ 117 public boolean isObject() { 118 return value instanceof Map; 119 } 120 121 /** 122 * Returns <jk>true</jk> if this node wraps a {@link List} (an array node). 123 * 124 * @return <jk>true</jk> if this is an array node. 125 */ 126 public boolean isArray() { 127 return value instanceof List; 128 } 129 130 /** 131 * Returns <jk>true</jk> if this node wraps a non-<jk>null</jk> scalar (not a {@link Map} or {@link List}). 132 * 133 * @return <jk>true</jk> if this is a value node. 134 */ 135 public boolean isValue() { 136 return value != null && ! (value instanceof Map) && ! (value instanceof List); 137 } 138 139 /** 140 * Returns <jk>true</jk> if this node wraps <jk>null</jk>. 141 * 142 * @return <jk>true</jk> if this is a null node. 143 */ 144 public boolean isNull() { 145 return value == null; 146 } 147 148 /** 149 * Returns the backing value converted to a {@link String}. 150 * 151 * @return The converted value, or <jk>null</jk> if this is not a value node or the value cannot be converted. 152 */ 153 public String asString() { 154 return asScalar(String.class); 155 } 156 157 /** 158 * Returns the backing value converted to an {@link Integer}. 159 * 160 * @return The converted value, or <jk>null</jk> if this is not a value node or the value cannot be converted. 161 */ 162 public Integer asInt() { 163 return asScalar(Integer.class); 164 } 165 166 /** 167 * Returns the backing value converted to a {@link Long}. 168 * 169 * @return The converted value, or <jk>null</jk> if this is not a value node or the value cannot be converted. 170 */ 171 public Long asLong() { 172 return asScalar(Long.class); 173 } 174 175 /** 176 * Returns the backing value converted to a {@link Double}. 177 * 178 * @return The converted value, or <jk>null</jk> if this is not a value node or the value cannot be converted. 179 */ 180 public Double asDouble() { 181 return asScalar(Double.class); 182 } 183 184 /** 185 * Returns the backing value converted to a {@link Boolean}. 186 * 187 * @return The converted value, or <jk>null</jk> if this is not a value node or the value cannot be converted. 188 */ 189 public Boolean asBoolean() { 190 return asScalar(Boolean.class); 191 } 192 193 /** 194 * Converts the raw backing value to the specified type. 195 * 196 * <p> 197 * Unlike the scalar accessors, this works on object nodes too (e.g. converting a {@link Map} object node to a bean). 198 * Routes through the same conversion machinery used by {@link MarshalledMap#get(String,Class)}. 199 * 200 * @param <T> The type to convert to. 201 * @param type The type to convert to. 202 * @return The converted value, or <jk>null</jk> if the backing value is <jk>null</jk> or cannot be converted. 203 */ 204 public <T> T as(Class<T> type) { 205 if (value == null) 206 return null; 207 try { 208 return MarshallingContext.DEFAULT_SESSION.convertToType(value, type); 209 } catch (InvalidDataConversionException e) { 210 return null; 211 } 212 } 213 214 /** 215 * Returns the raw backing value wrapped by this node. 216 * 217 * @return The raw backing value, or <jk>null</jk> if this is a <i>null</i> node. 218 */ 219 public Object value() { 220 return value; 221 } 222 223 /** 224 * Returns the child node for the specified key on an object node. 225 * 226 * @param name The key. 227 * @return The child node, or <jk>null</jk> if this is not an object node or the key is absent. 228 */ 229 public MarshalledNode get(String name) { 230 if (value instanceof Map<?,?> value2 && value2.containsKey(name)) 231 return new MarshalledNode(value2.get(name)); 232 return null; 233 } 234 235 /** 236 * Returns the element node at the specified index on an array node. 237 * 238 * @param index The zero-based index. 239 * @return The element node, or <jk>null</jk> if this is not an array node or the index is out of range. 240 */ 241 public MarshalledNode get(int index) { 242 if (value instanceof List<?> value2 && index >= 0 && index < value2.size()) 243 return new MarshalledNode(value2.get(index)); 244 return null; 245 } 246 247 /** 248 * Returns the number of entries (object) or elements (array) in this node. 249 * 250 * @return The size, or <c>0</c> if this is a value or null node. 251 */ 252 public int size() { 253 if (value instanceof Map<?,?> value2) 254 return value2.size(); 255 if (value instanceof List<?> value2) 256 return value2.size(); 257 return 0; 258 } 259 260 /** 261 * Adds or replaces an entry on an object node, mutating the backing {@link Map} in place. 262 * 263 * @param name The key. 264 * @param value The value. 265 * @return This object. 266 * @throws IllegalStateException If this is not an object node. 267 */ 268 public MarshalledNode put(String name, Object value) { 269 if (! isObject()) 270 throw isex("put(String,Object) is only valid on an object node."); 271 ((Map<String,Object>)this.value).put(name, value); 272 return this; 273 } 274 275 /** 276 * Appends a value on an array node, mutating the backing {@link List} in place. 277 * 278 * @param value The value to append. 279 * @return This object. 280 * @throws IllegalStateException If this is not an array node. 281 */ 282 public MarshalledNode add(Object value) { 283 if (! isArray()) 284 throw isex("add(Object) is only valid on an array node."); 285 ((List<Object>)this.value).add(value); 286 return this; 287 } 288 289 /** 290 * Returns a new node backed by a deep copy of this node's backing value. 291 * 292 * <p> 293 * Maps are copied into new {@link JsonMap} instances and lists into new {@link JsonList} instances (recursively); 294 * scalars and <jk>null</jk> are returned as-is (they are immutable). Mutating the copy does not affect the original 295 * and vice versa. 296 * 297 * @return A new independent node, never <jk>null</jk>. 298 */ 299 public MarshalledNode copy() { 300 return new MarshalledNode(deepCopy(value)); 301 } 302 303 /** 304 * Returns the node addressed by the specified <a class="doclink" href="https://datatracker.ietf.org/doc/html/rfc6901">RFC 6901</a> 305 * JSON Pointer relative to this node's backing value. 306 * 307 * <p> 308 * A read-miss (missing key, out-of-range index, or type mismatch) returns <jk>null</jk> so callers can null-check. 309 * If the addressed value exists and is JSON <jk>null</jk>, a null node is returned (use {@link #isNull()} to test it). 310 * The empty pointer addresses this node's whole backing value. 311 * 312 * @param pointer The RFC 6901 JSON Pointer. 313 * @return A node wrapping the addressed value, or <jk>null</jk> if the addressed value is absent. 314 */ 315 public MarshalledNode at(String pointer) { 316 return find(pointer).orElse(null); 317 } 318 319 /** 320 * Optional read variant of {@link #at(String)}. 321 * 322 * @param pointer The RFC 6901 JSON Pointer. 323 * @return The addressed node (present, possibly a null node), or an empty {@link Optional} if the addressed value is absent. 324 */ 325 public Optional<MarshalledNode> find(String pointer) { 326 var o = JsonPointer.of(pointer).evalOrMissing(value); 327 return JsonPointer.isMissing(o) ? oe() : o(new MarshalledNode(o)); 328 } 329 330 /** 331 * Writes the specified value at the location addressed by the specified 332 * <a class="doclink" href="https://datatracker.ietf.org/doc/html/rfc6901">RFC 6901</a> JSON Pointer, mutating the 333 * backing container in place and auto-vivifying missing intermediate containers. 334 * 335 * <p> 336 * Intermediate containers are created based on the next token (a numeric or <c>-</c> token implies a {@link JsonList}, 337 * otherwise a {@link JsonMap}); the <c>-</c> token on a list appends. Because the backing value is <jk>final</jk>, 338 * the empty (root) pointer cannot replace this node's whole document in place and is therefore rejected. 339 * 340 * @param pointer The RFC 6901 JSON Pointer. Must not be the root pointer. 341 * @param value The value to set. 342 * @return This object. 343 * @throws IllegalArgumentException If <c>pointer</c> is the empty/root pointer. 344 */ 345 public MarshalledNode set(String pointer, Object value) { 346 var p = JsonPointer.of(pointer); 347 if (p.isRoot()) 348 throw iaex("Cannot set the whole document via the root pointer on a node."); 349 p.set(this.value, value); 350 return this; 351 } 352 353 /** 354 * Removes the member or element addressed by the specified 355 * <a class="doclink" href="https://datatracker.ietf.org/doc/html/rfc6901">RFC 6901</a> JSON Pointer. 356 * 357 * @param pointer The RFC 6901 JSON Pointer. 358 * @return The removed value, or <jk>null</jk> if the addressed member/element was absent. 359 */ 360 public Object remove(String pointer) { 361 return JsonPointer.of(pointer).remove(value); 362 } 363 364 private <T> T asScalar(Class<T> type) { 365 if (! isValue()) 366 return null; 367 try { 368 return MarshallingContext.DEFAULT_SESSION.convertToType(value, type); 369 } catch (InvalidDataConversionException e) { 370 return null; 371 } 372 } 373 374 private static Object deepCopy(Object v) { 375 if (v instanceof Map<?,?> v2) { 376 var r = new JsonMap(); 377 v2.forEach((k, val) -> r.put(String.valueOf(k), deepCopy(val))); 378 return r; 379 } 380 if (v instanceof List<?> v2) { 381 var r = new JsonList(); 382 v2.forEach(e -> r.add(deepCopy(e))); 383 return r; 384 } 385 return v; 386 } 387}