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}