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.objecttools;
018
019import static java.net.HttpURLConnection.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021import static org.apache.juneau.commons.utils.StringUtils.*;
022
023import java.lang.reflect.*;
024import java.util.*;
025
026import org.apache.juneau.marshall.*;
027import org.apache.juneau.marshall.collections.*;
028import org.apache.juneau.marshall.parser.*;
029
030/**
031 * Traversal engine for performing standard REST operations (GET, PUT, POST, DELETE) against nodes in a POJO model.
032 *
033 * <p>
034 * <b>Internal API:</b> This class is the relocated traversal engine behind the {@code Marshalled*} path-addressing
035 * methods and the Swagger/OpenApi {@code $ref} resolver. It is public only for cross-module access and is <b>not</b> a
036 * supported public API &mdash; do not use it directly.
037 *
038 * <p>
039 * Nodes in the POJO model are addressed using URLs.
040 *
041 * <p>
042 * A POJO model is defined as a tree model where nodes consist of consisting of the following:
043 * <ul class='spaced-list'>
044 *    <li>
045 *       {@link Map Maps} and Java beans representing JSON objects.
046 *    <li>
047 *       {@link Collection Collections} and arrays representing JSON arrays.
048 *    <li>
049 *       Java beans.
050 * </ul>
051 *
052 * <p>
053 * Leaves of the tree can be any type of object.
054 *
055 * <p>
056 * Use {@link #get(String) get()} to retrieve an element from a JSON tree.
057 * <br>Use {@link #put(String,Object) put()} to create (or overwrite) an element in a JSON tree.
058 * <br>Use {@link #post(String,Object) post()} to add an element to a list in a JSON tree.
059 * <br>Use {@link #delete(String) delete()} to remove an element from a JSON tree.
060 *
061 * <p>
062 * Leading slashes in URLs are ignored.
063 * So <js>"/xxx/yyy/zzz"</js> and <js>"xxx/yyy/zzz"</js> are considered identical.
064 *
065 * <h5 class='section'>Example:</h5>
066 * <p class='bjava'>
067 *    <jc>// Construct an unstructured POJO model</jc>
068 *    JsonMap <jv>map</jv> = JsonMap.<jsm>ofText</jsm>(<js>""</js>
069 *       + <js>"{"</js>
070 *       + <js>"  name:'John Smith', "</js>
071 *       + <js>"  address:{ "</js>
072 *       + <js>"     streetAddress:'21 2nd Street', "</js>
073 *       + <js>"     city:'New York', "</js>
074 *       + <js>"     state:'NY', "</js>
075 *       + <js>"     postalCode:10021 "</js>
076 *       + <js>"  }, "</js>
077 *       + <js>"  phoneNumbers:[ "</js>
078 *       + <js>"     '212 555-1111', "</js>
079 *       + <js>"     '212 555-2222' "</js>
080 *       + <js>"  ], "</js>
081 *       + <js>"  additionalInfo:null, "</js>
082 *       + <js>"  remote:false, "</js>
083 *       + <js>"  height:62.4, "</js>
084 *       + <js>"  'fico score':' &gt; 640' "</js>
085 *       + <js>"} "</js>
086 *    );
087 *
088 *    <jc>// Wrap Map inside a PathTraversal object</jc>
089 *    PathTraversal <jv>johnSmith</jv> = PathTraversal.<jsm>create</jsm>(<jv>map</jv>);
090 *
091 *    <jc>// Get a simple value at the top level</jc>
092 *    <jc>// "John Smith"</jc>
093 *    String <jv>name</jv> = <jv>johnSmith</jv>.getString(<js>"name"</js>);
094 *
095 *    <jc>// Change a simple value at the top level</jc>
096 *    <jv>johnSmith</jv>.put(<js>"name"</js>, <js>"The late John Smith"</js>);
097 *
098 *    <jc>// Get a simple value at a deep level</jc>
099 *    <jc>// "21 2nd Street"</jc>
100 *    String <jv>streetAddress</jv> = <jv>johnSmith</jv>.getString(<js>"address/streetAddress"</js>);
101 *
102 *    <jc>// Set a simple value at a deep level</jc>
103 *    <jv>johnSmith</jv>.put(<js>"address/streetAddress"</js>, <js>"101 Cemetery Way"</js>);
104 *
105 *    <jc>// Get entries in a list</jc>
106 *    <jc>// "212 555-1111"</jc>
107 *    String <jv>firstPhoneNumber</jv> = <jv>johnSmith</jv>.getString(<js>"phoneNumbers/0"</js>);
108 *
109 *    <jc>// Add entries to a list</jc>
110 *    <jv>johnSmith</jv>.post(<js>"phoneNumbers"</js>, <js>"212 555-3333"</js>);
111 *
112 *    <jc>// Delete entries from a model</jc>
113 *    <jv>johnSmith</jv>.delete(<js>"fico score"</js>);
114 *
115 *    <jc>// Add entirely new structures to the tree</jc>
116 *    JsonMap <jv>medicalInfo</jv> = JsonMap.<jsm>ofText</jsm>(<js>""</js>
117 *       + <js>"{"</js>
118 *       + <js>"  currentStatus: 'deceased',"</js>
119 *       + <js>"  health: 'non-existent',"</js>
120 *       + <js>"  creditWorthiness: 'not good'"</js>
121 *       + <js>"}"</js>
122 *    );
123 *    <jv>johnSmith</jv>.put(<js>"additionalInfo/medicalInfo"</js>, <jv>medicalInfo</jv>);
124 * </p>
125 *
126 * <p>
127 * In the special case of collections/arrays of maps/beans, a special XPath-like selector notation can be used in lieu
128 * of index numbers on GET requests to return a map/bean with a specified attribute value.
129 * <br>The syntax is {@code @attr=val}, where attr is the attribute name on the child map, and val is the matching value.
130 *
131 * <h5 class='section'>Example:</h5>
132 * <p class='bjava'>
133 *    <jc>// Get map/bean with name attribute value of 'foo' from a list of items</jc>
134 *    Map <jv>map</jv> = <jv>pathTraversal</jv>.getMap(<js>"/items/@name=foo"</js>);
135 * </p>
136 */
137@SuppressWarnings({
138   "java:S1452", // getList and getMap return List<?>/Map<?,?> because the element types are unknown until the caller converts them
139   "java:S3776", // service() walks the URL path across maps, lists and arrays for every GET/PUT/POST/DELETE case
140   "rawtypes", // Raw types necessary for generic type handling
141   "unchecked" // Type erasure requires unchecked operations for generic collections
142})
143public class PathTraversal {
144   // Internal traversal node — never serialized; 'parent' is an upward tree pointer used only to walk back up the model during PUT/POST/DELETE rewrites.
145   class JsonNode {
146      Object o;
147      ClassMeta cm;
148      JsonNode parent;
149      String keyName;
150
151      JsonNode(JsonNode parent, String keyName, Object o, ClassMeta cm) {
152         this.o = o;
153         this.keyName = keyName;
154         this.parent = parent;
155         if (cm == null || cm.isObject()) {
156            if (o == null)
157               cm = session.object();
158            else
159               cm = session.getClassMetaForObject(o);
160         }
161         this.cm = cm;
162      }
163   }
164
165   /** The list of possible request types. */
166   private static final int GET = 1;
167   private static final int PUT = 2;
168   private static final int POST = 3;
169   private static final int DELETE = 4;
170
171   /**
172    * Static creator.
173    * @param o The object being wrapped.
174    * @return A new {@link PathTraversal} object.
175    */
176   public static PathTraversal create(Object o) {
177      return new PathTraversal(o);
178   }
179
180   /**
181    * Static creator.
182    * @param o The object being wrapped.
183    * @param parser The parser to use for parsing arguments and converting objects to the correct data type.
184    * @return A new {@link PathTraversal} object.
185    */
186   public static PathTraversal create(Object o, ReaderParser parser) {
187      return new PathTraversal(o, parser);
188   }
189
190   /** Handle nulls and strip off leading '/' char. */
191   private static String normalizeUrl(String url) {
192
193      // Interpret nulls and blanks the same (i.e. as addressing the root itself)
194      if (url == null)
195         url = "";
196
197      // Strip off leading slash if present.
198      if (ine(url) && url.charAt(0) == '/')
199         url = url.substring(1);
200
201      return url;
202   }
203
204   private static int parseInt(String key) {
205      try {
206         return Integer.parseInt(key);
207      } catch (@SuppressWarnings("unused") NumberFormatException e) {
208         throw new PathTraversalException(HTTP_BAD_REQUEST, "Cannot address an item in an array with a non-integer key '%s'", key);
209      }
210   }
211
212   private static Object[] removeArrayEntry(Object o, int index) {
213      var a = (Object[])o;
214      // Shrink the array.
215      var a2 = (Object[])Array.newInstance(a.getClass().getComponentType(), a.length - 1);
216      System.arraycopy(a, 0, a2, 0, index);
217      System.arraycopy(a, index + 1, a2, index, a.length - index - 1);
218      return a2;
219   }
220
221   final MarshallingSession session;
222
223   /** If true, the root cannot be overwritten */
224   private boolean rootLocked;
225
226   /** The root of the model. */
227   private JsonNode root;
228
229   /**
230    * Create a new instance of a REST interface over the specified object.
231    *
232    * <p>
233    * Uses {@link MarshallingContext#DEFAULT} for working with Java beans.
234    *
235    * @param o The object to be wrapped.
236    */
237   public PathTraversal(Object o) {
238      this(o, null);
239   }
240
241   /**
242    * Create a new instance of a REST interface over the specified object.
243    *
244    * <p>
245    * The parser is used as the bean context.
246    *
247    * @param o The object to be wrapped.
248    * @param parser The parser to use for parsing arguments and converting objects to the correct data type.
249    */
250   public PathTraversal(Object o, ReaderParser parser) {
251      this.session = parser == null ? MarshallingContext.DEFAULT_SESSION : parser.getMarshallingContext().getSession();
252      this.root = new JsonNode(null, null, o, session.object());
253   }
254
255   /**
256    * Remove an element from a POJO model.
257    *
258    * <p>
259    * If the element does not exist, no action is taken.
260    *
261    * @param url
262    *    The URL of the element being deleted.
263    *    If <jk>null</jk> or blank, the root itself is deleted.
264    * @return The removed element, or null if that element does not exist.
265    */
266   public Object delete(String url) {
267      return service(DELETE, url, null);
268   }
269
270   /**
271    * Retrieves the element addressed by the URL.
272    *
273    * @param url
274    *    The URL of the element to retrieve.
275    *    <br>If <jk>null</jk> or blank, returns the root.
276    * @return The addressed element, or <jk>null</jk> if that element does not exist in the tree.
277    */
278   public Object get(String url) {
279      return getWithDefault(url, null);
280   }
281
282   /**
283    * Retrieves the element addressed by the URL as the specified object type.
284    *
285    * <p>
286    * Will convert object to the specified type per {@link MarshallingSession#convertToType(Object, Class)}.
287    *
288    * <h5 class='section'>Examples:</h5>
289    * <p class='bjava'>
290    *    PathTraversal <jv>pathTraversal</jv> = <jk>new</jk> PathTraversal(<jv>object</jv>);
291    *
292    *    <jc>// Value converted to a string.</jc>
293    *    String <jv>string</jv> = <jv>pathTraversal</jv>.get(<js>"path/to/string"</js>, String.<jk>class</jk>);
294    *
295    *    <jc>// Value converted to a bean.</jc>
296    *    MyBean <jv>bean</jv> = <jv>pathTraversal</jv>.get(<js>"path/to/bean"</js>, MyBean.<jk>class</jk>);
297    *
298    *    <jc>// Value converted to a bean array.</jc>
299    *    MyBean[] <jv>beanArray</jv> = <jv>pathTraversal</jv>.get(<js>"path/to/beanarray"</js>, MyBean[].<jk>class</jk>);
300    *
301    *    <jc>// Value converted to a linked-list of objects.</jc>
302    *    List <jv>list</jv> = <jv>pathTraversal</jv>.get(<js>"path/to/list"</js>, LinkedList.<jk>class</jk>);
303    *
304    *    <jc>// Value converted to a map of object keys/values.</jc>
305    *    Map <jv>map</jv> = <jv>pathTraversal</jv>.get(<js>"path/to/map"</js>, TreeMap.<jk>class</jk>);
306    * </p>
307    *
308    * @param url
309    *    The URL of the element to retrieve.
310    *    If <jk>null</jk> or blank, returns the root.
311    * @param type The specified object type.
312    *
313    * @param <T> The specified object type.
314    * @return The addressed element, or null if that element does not exist in the tree.
315    */
316   public <T> T get(String url, Class<T> type) {
317      return getWithDefault(url, null, type);
318   }
319
320   /**
321    * Retrieves the element addressed by the URL as the specified object type.
322    *
323    * <p>
324    * Will convert object to the specified type per {@link MarshallingSession#convertToType(Object, Class)}.
325    *
326    * <p>
327    * The type can be a simple type (e.g. beans, strings, numbers) or parameterized type (collections/maps).
328    *
329    * <h5 class='section'>Examples:</h5>
330    * <p class='bjava'>
331    *    PathTraversal <jv>pathTraversal</jv> = <jk>new</jk> PathTraversal(<jv>object</jv>);
332    *
333    *    <jc>// Value converted to a linked-list of strings.</jc>
334    *    List&lt;String&gt; <jv>list1</jv> = <jv>pathTraversal</jv>.get(<js>"path/to/list1"</js>, LinkedList.<jk>class</jk>, String.<jk>class</jk>);
335    *
336    *    <jc>// Value converted to a linked-list of beans.</jc>
337    *    List&lt;MyBean&gt; <jv>list2</jv> = <jv>pathTraversal</jv>.get(<js>"path/to/list2"</js>, LinkedList.<jk>class</jk>, MyBean.<jk>class</jk>);
338    *
339    *    <jc>// Value converted to a linked-list of linked-lists of strings.</jc>
340    *    List&lt;List&lt;String&gt;&gt; <jv>list3</jv> = <jv>pathTraversal</jv>.get(<js>"path/to/list3"</js>, LinkedList.<jk>class</jk>, LinkedList.<jk>class</jk>, String.<jk>class</jk>);
341    *
342    *    <jc>// Value converted to a map of string keys/values.</jc>
343    *    Map&lt;String,String&gt; <jv>map1</jv> = <jv>pathTraversal</jv>.get(<js>"path/to/map1"</js>, TreeMap.<jk>class</jk>, String.<jk>class</jk>, String.<jk>class</jk>);
344    *
345    *    <jc>// Value converted to a map containing string keys and values of lists containing beans.</jc>
346    *    Map&lt;String,List&lt;MyBean&gt;&gt; <jv>map2</jv> = <jv>pathTraversal</jv>.get(<js>"path/to/map2"</js>, TreeMap.<jk>class</jk>, String.<jk>class</jk>, List.<jk>class</jk>, MyBean.<jk>class</jk>);
347    * </p>
348    *
349    * <p>
350    * <c>Collection</c> classes are assumed to be followed by zero or one objects indicating the element type.
351    *
352    * <p>
353    * <c>Map</c> classes are assumed to be followed by zero or two meta objects indicating the key and value types.
354    *
355    * <p>
356    * The array can be arbitrarily long to indicate arbitrarily complex data structures.
357    *
358    * <h5 class='section'>Notes:</h5><ul>
359    *    <li class='note'>
360    *       Use the {@link #get(String, Class)} method instead if you don't need a parameterized map/collection.
361    * </ul>
362    *
363    * @param url
364    *    The URL of the element to retrieve.
365    *    If <jk>null</jk> or blank, returns the root.
366    * @param type The specified object type.
367    * @param args The specified object parameter types.
368    *
369    * @param <T> The specified object type.
370    * @return The addressed element, or null if that element does not exist in the tree.
371    */
372   public <T> T get(String url, Type type, Type...args) {
373      return getWithDefault(url, null, type, args);
374   }
375
376   /**
377    * Returns the specified entry value converted to a {@link Boolean}.
378    *
379    * <p>
380    * Shortcut for <code>get(Boolean.<jk>class</jk>, key)</code>.
381    *
382    * @param url The key.
383    * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key.
384    * @throws InvalidDataConversionException If value cannot be converted.
385    */
386   public Boolean getBoolean(String url) {
387      return get(url, Boolean.class);
388   }
389
390   /**
391    * Returns the specified entry value converted to a {@link Boolean}.
392    *
393    * <p>
394    * Shortcut for <code>get(Boolean.<jk>class</jk>, key, defVal)</code>.
395    *
396    * @param url The key.
397    * @param defVal The default value if the map doesn't contain the specified mapping.
398    * @return The converted value, or the default value if the map contains no mapping for this key.
399    * @throws InvalidDataConversionException If value cannot be converted.
400    */
401   public Boolean getBoolean(String url, Boolean defVal) {
402      return getWithDefault(url, defVal, Boolean.class);
403   }
404
405   /**
406    * Returns the class type of the object at the specified URL.
407    *
408    * @param url The URL.
409    * @return The class type, or <jk>null</jk> if the addressed element does not exist in the tree.
410    */
411   public ClassMeta getClassMeta(String url) {
412      var n = getNode(normalizeUrl(url), root);
413      if (n == null)
414         return null;
415      return n.cm;
416   }
417
418   /**
419    * Returns the specified entry value converted to an {@link Integer}.
420    *
421    * <p>
422    * Shortcut for <code>get(Integer.<jk>class</jk>, key)</code>.
423    *
424    * @param url The key.
425    * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key.
426    * @throws InvalidDataConversionException If value cannot be converted.
427    */
428   public Integer getInt(String url) {
429      return get(url, Integer.class);
430   }
431
432   /**
433    * Returns the specified entry value converted to an {@link Integer}.
434    *
435    * <p>
436    * Shortcut for <code>get(Integer.<jk>class</jk>, key, defVal)</code>.
437    *
438    * @param url The key.
439    * @param defVal The default value if the map doesn't contain the specified mapping.
440    * @return The converted value, or the default value if the map contains no mapping for this key.
441    * @throws InvalidDataConversionException If value cannot be converted.
442    */
443   public Integer getInt(String url, Integer defVal) {
444      return getWithDefault(url, defVal, Integer.class);
445   }
446
447   /**
448    * Returns the specified entry value converted to a {@link JsonList}.
449    *
450    * <p>
451    * Shortcut for <code>get(JsonList.<jk>class</jk>, key)</code>.
452    *
453    * @param url The key.
454    * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key.
455    * @throws InvalidDataConversionException If value cannot be converted.
456    */
457   public JsonList getJsonList(String url) {
458      return get(url, JsonList.class);
459   }
460
461   /**
462    * Returns the specified entry value converted to a {@link JsonList}.
463    *
464    * <p>
465    * Shortcut for <code>get(JsonList.<jk>class</jk>, key, defVal)</code>.
466    *
467    * @param url The key.
468    * @param defVal The default value if the map doesn't contain the specified mapping.
469    * @return The converted value, or the default value if the map contains no mapping for this key.
470    * @throws InvalidDataConversionException If value cannot be converted.
471    */
472   public JsonList getJsonList(String url, JsonList defVal) {
473      return getWithDefault(url, defVal, JsonList.class);
474   }
475
476   /**
477    * Returns the specified entry value converted to a {@link Map}.
478    *
479    * <p>
480    * Shortcut for <code>get(JsonMap.<jk>class</jk>, key)</code>.
481    *
482    * @param url The key.
483    * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key.
484    * @throws InvalidDataConversionException If value cannot be converted.
485    */
486   public JsonMap getJsonMap(String url) {
487      return get(url, JsonMap.class);
488   }
489
490   /**
491    * Returns the specified entry value converted to a {@link JsonMap}.
492    *
493    * <p>
494    * Shortcut for <code>get(JsonMap.<jk>class</jk>, key, defVal)</code>.
495    *
496    * @param url The key.
497    * @param defVal The default value if the map doesn't contain the specified mapping.
498    * @return The converted value, or the default value if the map contains no mapping for this key.
499    * @throws InvalidDataConversionException If value cannot be converted.
500    */
501   public JsonMap getJsonMap(String url, JsonMap defVal) {
502      return getWithDefault(url, defVal, JsonMap.class);
503   }
504
505   /**
506    * Returns the specified entry value converted to a {@link List}.
507    *
508    * <p>
509    * Shortcut for <code>get(List.<jk>class</jk>, key)</code>.
510    *
511    * @param url The key.
512    * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key.
513    * @throws InvalidDataConversionException If value cannot be converted.
514    */
515   public List<?> getList(String url) {
516      return get(url, List.class);
517   }
518
519   /**
520    * Returns the specified entry value converted to a {@link List}.
521    *
522    * <p>
523    * Shortcut for <code>get(List.<jk>class</jk>, key, defVal)</code>.
524    *
525    * @param url The key.
526    * @param defVal The default value if the map doesn't contain the specified mapping.
527    * @return The converted value, or the default value if the map contains no mapping for this key.
528    * @throws InvalidDataConversionException If value cannot be converted.
529    */
530   public List<?> getList(String url, List<?> defVal) {
531      return getWithDefault(url, defVal, List.class);
532   }
533
534   /**
535    * Returns the specified entry value converted to a {@link Long}.
536    *
537    * <p>
538    * Shortcut for <code>get(Long.<jk>class</jk>, key)</code>.
539    *
540    * @param url The key.
541    * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key.
542    * @throws InvalidDataConversionException If value cannot be converted.
543    */
544   public Long getLong(String url) {
545      return get(url, Long.class);
546   }
547
548   /**
549    * Returns the specified entry value converted to a {@link Long}.
550    *
551    * <p>
552    * Shortcut for <code>get(Long.<jk>class</jk>, key, defVal)</code>.
553    *
554    * @param url The key.
555    * @param defVal The default value if the map doesn't contain the specified mapping.
556    * @return The converted value, or the default value if the map contains no mapping for this key.
557    * @throws InvalidDataConversionException If value cannot be converted.
558    */
559   public Long getLong(String url, Long defVal) {
560      return getWithDefault(url, defVal, Long.class);
561   }
562
563   /**
564    * Returns the specified entry value converted to a {@link Map}.
565    *
566    * <p>
567    * Shortcut for <code>get(Map.<jk>class</jk>, key)</code>.
568    *
569    * @param url The key.
570    * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key.
571    * @throws InvalidDataConversionException If value cannot be converted.
572    */
573   public Map<?,?> getMap(String url) {
574      return get(url, Map.class);
575   }
576
577   /**
578    * Returns the specified entry value converted to a {@link Map}.
579    *
580    * <p>
581    * Shortcut for <code>get(Map.<jk>class</jk>, key, defVal)</code>.
582    *
583    * @param url The key.
584    * @param defVal The default value if the map doesn't contain the specified mapping.
585    * @return The converted value, or the default value if the map contains no mapping for this key.
586    * @throws InvalidDataConversionException If value cannot be converted.
587    */
588   public Map<?,?> getMap(String url, Map<?,?> defVal) {
589      return getWithDefault(url, defVal, Map.class);
590   }
591
592   /**
593    * The root object that was passed into the constructor of this method.
594    *
595    * @return The root object.
596    */
597   public Object getRootObject() { return root.o; }
598
599   /**
600    * Returns the specified entry value converted to a {@link String}.
601    *
602    * <p>
603    * Shortcut for <code>get(String.<jk>class</jk>, key)</code>.
604    *
605    * @param url The key.
606    * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key.
607    */
608   public String getString(String url) {
609      return get(url, String.class);
610   }
611
612   /**
613    * Returns the specified entry value converted to a {@link String}.
614    *
615    * <p>
616    * Shortcut for <code>get(String.<jk>class</jk>, key, defVal)</code>.
617    *
618    * @param url The key.
619    * @param defVal The default value if the map doesn't contain the specified mapping.
620    * @return The converted value, or the default value if the map contains no mapping for this key.
621    */
622   public String getString(String url, String defVal) {
623      return getWithDefault(url, defVal, String.class);
624   }
625
626   /**
627    * Retrieves the element addressed by the URL.
628    *
629    * @param url
630    *    The URL of the element to retrieve.
631    *    <br>If <jk>null</jk> or blank, returns the root.
632    * @param defVal The default value if the map doesn't contain the specified mapping.
633    * @return The addressed element, or null if that element does not exist in the tree.
634    */
635   public Object getWithDefault(String url, Object defVal) {
636      var o = service(GET, url, null);
637      return o == null ? defVal : o;
638   }
639
640   /**
641    * Same as {@link #get(String, Class)} but returns a default value if the addressed element is null or non-existent.
642    *
643    * @param url
644    *    The URL of the element to retrieve.
645    *    If <jk>null</jk> or blank, returns the root.
646    * @param def The default value if addressed item does not exist.
647    * @param type The specified object type.
648    *
649    * @param <T> The specified object type.
650    * @return The addressed element, or null if that element does not exist in the tree.
651    */
652   public <T> T getWithDefault(String url, T def, Class<T> type) {
653      var o = service(GET, url, null);
654      if (o == null)
655         return def;
656      return session.convertToType(o, type);
657   }
658
659   /**
660    * Same as {@link #get(String,Type,Type[])} but returns a default value if the addressed element is null or non-existent.
661    *
662    * @param url
663    *    The URL of the element to retrieve.
664    *    If <jk>null</jk> or blank, returns the root.
665    * @param def The default value if addressed item does not exist.
666    * @param type The specified object type.
667    * @param args The specified object parameter types.
668    *
669    * @param <T> The specified object type.
670    * @return The addressed element, or null if that element does not exist in the tree.
671    */
672   public <T> T getWithDefault(String url, T def, Type type, Type...args) {
673      var o = service(GET, url, null);
674      if (o == null)
675         return def;
676      return session.convertToType(o, type, args);
677   }
678
679   /**
680    * Adds a value to a list element in a POJO model.
681    *
682    * <p>
683    * The URL is the address of the list being added to.
684    *
685    * <p>
686    * If the list does not already exist, it will be created.
687    *
688    * <p>
689    * This method expands the POJO model as necessary to create the new element.
690    *
691    * <h5 class='section'>Notes:</h5><ul>
692    *    <li class='note'>
693    *       You can only post to three types of nodes:
694    *       <ul>
695    *          <li>{@link List Lists}
696    *          <li>{@link Map Maps} containing integers as keys (i.e sparse arrays)
697    *          <li>arrays
698    *       </ul>
699    * </ul>
700    *
701    * @param url
702    *    The URL of the element being added to.
703    *    If <jk>null</jk> or blank, the root itself (assuming it's one of the types specified above) is added to.
704    * @param val The value being added.
705    * @return The URL of the element that was added, or <jk>null</jk> if the value was added to a non-indexable (non-{@link List}) collection.
706    */
707   public String post(String url, Object val) {
708      return (String)service(POST, url, val);
709   }
710
711   /**
712    * Sets/replaces the element addressed by the URL.
713    *
714    * <p>
715    * This method expands the POJO model as necessary to create the new element.
716    *
717    * @param url
718    *    The URL of the element to create.
719    *    If <jk>null</jk> or blank, the root itself is replaced with the specified value.
720    * @param val The value being set.  Value can be of any type.
721    * @return The previously addressed element, or <jk>null</jk> if the element did not previously exist.
722    */
723   public Object put(String url, Object val) {
724      return service(PUT, url, val);
725   }
726
727   /**
728    * Call this method to prevent the root object from being overwritten on <c>put("", xxx);</c> calls.
729    *
730    * @return This object.
731    */
732   public PathTraversal setRootLocked() {
733      this.rootLocked = true;
734      return this;
735   }
736
737   @Override /* Overridden from Object */
738   public String toString() {
739      return String.valueOf(root.o);
740   }
741
742   private Object[] addArrayEntry(Object o, Object val, ClassMeta componentType) {
743      var a = (Object[])o;
744      // Expand out the array.
745      var a2 = (Object[])Array.newInstance(a.getClass().getComponentType(), a.length + 1);
746      System.arraycopy(a, 0, a2, 0, a.length);
747      a2[a.length] = convert(val, componentType);
748      return a2;
749   }
750
751   private Object convert(Object in, ClassMeta cm) {
752      if (cm == null)
753         return in;
754      if (cm.isBean() && in instanceof Map in2)
755         return session.convertToType(in2, cm);
756      return in;
757   }
758
759   /*
760    * Workhorse method.
761    */
762   @SuppressWarnings({
763      "java:S6541" // Cognitive complexity acceptable for REST operation routing logic
764   })
765   private Object service(int method, String url, Object val) throws PathTraversalException {
766
767      url = normalizeUrl(url);
768
769      if (method == GET) {
770         var p = getNode(url, root);
771         return p == null ? null : p.o;
772      }
773
774      // Get the url of the parent and the property name of the addressed object.
775      var i = url.lastIndexOf('/');
776      var parentUrl = (i == -1 ? null : url.substring(0, i));
777      var childKey = (i == -1 ? url : url.substring(i + 1));
778
779      if (method == PUT) {
780         if (url.isEmpty()) {
781            if (rootLocked)
782               throw new PathTraversalException(HTTP_FORBIDDEN, "Cannot overwrite root object");
783            var o = root.o;
784            root = new JsonNode(null, null, val, session.object());
785            return o;
786         }
787         var n = (parentUrl == null ? root : getNode(parentUrl, root));
788         if (n == null)
789            throw new PathTraversalException(HTTP_NOT_FOUND, "Node at URL '%s' not found.", parentUrl);
790         if (n.o == null && n.cm.isObject())
791            throw new PathTraversalException(HTTP_NOT_FOUND, "Node at URL '%s' not found.", parentUrl);
792         var cm = n.cm;
793         var o = n.o;
794         if (cm.isMap())
795            return ((Map)o).put(childKey, convert(val, cm.getValueType()));
796         if (cm.isCollection() && o instanceof List o2)
797            return o2.set(parseInt(childKey), convert(val, cm.getElementType()));
798         if (cm.isCollection())
799            throw new PathTraversalException(HTTP_BAD_REQUEST, "Cannot PUT to indexed position in non-List collection '%s' of type '%s'", url, cm);
800         if (cm.isArray()) {
801            o = setArrayEntry(n.o, parseInt(childKey), val, cm.getElementType());
802            var pct = n.parent.cm;
803            var po = n.parent.o;
804            if (pct.isMap()) {
805               ((Map)po).put(n.keyName, o);
806               return url;
807            }
808            if (pct.isBean()) {
809               var m = session.toBeanMap(po);
810               m.put(n.keyName, o);
811               return url;
812            }
813            throw new PathTraversalException(HTTP_BAD_REQUEST, "Cannot perform PUT on '%s' with parent node type '%s'", url, pct);
814         }
815         if (cm.isBean())
816            return session.toBeanMap(o).put(childKey, val);
817         throw new PathTraversalException(HTTP_BAD_REQUEST, "Cannot perform PUT on '%s' whose parent is of type '%s'", url, cm);
818      }
819
820      if (method == POST) {
821         // Handle POST to root special
822         if (url.isEmpty()) {
823            var cm = root.cm;
824            var o = root.o;
825            if (cm.isCollection()) {
826               var c = (Collection)o;
827               c.add(convert(val, cm.getElementType()));
828               return (c instanceof List c2 ? url + "/" + (c2.size() - 1) : null);
829            }
830            if (cm.isArray()) {
831               var o2 = addArrayEntry(o, val, cm.getElementType());
832               root = new JsonNode(null, null, o2, null);
833               return url + "/" + (o2.length - 1);
834            }
835            throw new PathTraversalException(HTTP_BAD_REQUEST, "Cannot perform POST on '%s' of type '%s'", url, cm);
836         }
837         var n = getNode(url, root);
838         if (n == null)
839            throw new PathTraversalException(HTTP_NOT_FOUND, "Node at URL '%s' not found.", url);
840         if (n.o == null && n.cm.isObject())
841            throw new PathTraversalException(HTTP_NOT_FOUND, "Node at URL '%s' not found.", url);
842         var cm = n.cm;
843         var o = n.o;
844         if (cm.isArray()) {
845            var o2 = addArrayEntry(o, val, cm.getElementType());
846            var pct = n.parent.cm;
847            var po = n.parent.o;
848            if (pct.isMap()) {
849               ((Map)po).put(childKey, o2);
850               return url + "/" + (o2.length - 1);
851            }
852            if (pct.isBean()) {
853               var m = session.toBeanMap(po);
854               m.put(childKey, o2);
855               return url + "/" + (o2.length - 1);
856            }
857            throw new PathTraversalException(HTTP_BAD_REQUEST, "Cannot perform POST on '%s' with parent node type '%s'", url, pct);
858         }
859         if (cm.isCollection()) {
860            var c = (Collection)o;
861            c.add(convert(val, cm.getElementType()));
862            return (c instanceof List c2 ? url + "/" + (c2.size() - 1) : null);
863         }
864         throw new PathTraversalException(HTTP_BAD_REQUEST, "Cannot perform POST on '%s' of type '%s'", url, cm);
865      }
866
867      if (method == DELETE) {
868         if (url.isEmpty()) {
869            if (rootLocked)
870               throw new PathTraversalException(HTTP_FORBIDDEN, "Cannot overwrite root object");
871            var o = root.o;
872            root = new JsonNode(null, null, null, session.object());
873            return o;
874         }
875         var n = (parentUrl == null ? root : getNode(parentUrl, root));
876         if (n == null)
877            throw new PathTraversalException(HTTP_NOT_FOUND, "Node at URL '%s' not found.", parentUrl);
878         if (n.o == null && n.cm.isObject())
879            throw new PathTraversalException(HTTP_NOT_FOUND, "Node at URL '%s' not found.", parentUrl);
880         var cm = n.cm;
881         var o = n.o;
882         if (cm.isMap())
883            return ((Map)o).remove(childKey);
884         if (cm.isCollection() && o instanceof List o2)
885            return o2.remove(parseInt(childKey));
886         if (cm.isCollection())
887            throw new PathTraversalException(HTTP_BAD_REQUEST, "Cannot DELETE indexed position in non-List collection '%s' of type '%s'", url, cm);
888         if (cm.isArray()) {
889            int index = parseInt(childKey);
890            var old = ((Object[])o)[index];
891            var o2 = removeArrayEntry(o, index);
892            var pct = n.parent.cm;
893            var po = n.parent.o;
894            if (pct.isMap()) {
895               ((Map)po).put(n.keyName, o2);
896               return old;
897            }
898            if (pct.isBean()) {
899               var m = session.toBeanMap(po);
900               m.put(n.keyName, o2);
901               return old;
902            }
903            throw new PathTraversalException(HTTP_BAD_REQUEST, "Cannot perform POST on '%s' with parent node type '%s'", url, pct);
904         }
905         if (cm.isBean())
906            return session.toBeanMap(o).put(childKey, null);
907         throw new PathTraversalException(HTTP_BAD_REQUEST, "Cannot perform PUT on '%s' whose parent is of type '%s'", url, cm);
908      }
909
910      return null;   // Never gets here.
911   }
912
913   private Object[] setArrayEntry(Object o, int index, Object val, ClassMeta componentType) {
914      var a = (Object[])o;
915      if (a.length <= index) {
916         // Expand out the array.
917         var a2 = (Object[])Array.newInstance(a.getClass().getComponentType(), index + 1);
918         System.arraycopy(a, 0, a2, 0, a.length);
919         a = a2;
920      }
921      a[index] = convert(val, componentType);
922      return a;
923   }
924
925   JsonNode getNode(String url, JsonNode n) {
926      if (isEmpty(url))
927         return n;
928      int i = url.indexOf('/');
929      String parentKey;
930      String childUrl = null;
931      if (i == -1) {
932         parentKey = url;
933      } else {
934         parentKey = url.substring(0, i);
935         childUrl = url.substring(i + 1);
936      }
937
938      var o = n.o;
939      Object o2 = null;
940      var cm = n.cm;
941      ClassMeta ct2 = null;
942      if (o == null)
943         return null;
944      if (cm.isMap()) {
945         o2 = ((Map)o).get(parentKey);
946         ct2 = cm.getValueType();
947      } else if (cm.isCollection() && o instanceof List o3) {
948         var key = parseInt(parentKey);
949         if (o3.size() <= key)
950            return null;
951         o2 = o3.get(key);
952         ct2 = cm.getElementType();
953      } else if (cm.isArray()) {
954         var key = parseInt(parentKey);
955         var a = ((Object[])o);
956         if (a.length <= key)
957            return null;
958         o2 = a[key];
959         ct2 = cm.getElementType();
960      } else if (cm.isBean()) {
961         var m = session.toBeanMap(o);
962         o2 = m.get(parentKey);
963         var pMeta = m.getPropertyMeta(parentKey);
964         if (pMeta == null)
965            throw new PathTraversalException(HTTP_BAD_REQUEST, "Unknown property '%s' encountered while trying to parse into class '%s'", parentKey, m.getBeanInfo());
966         ct2 = (ClassMeta) pMeta.getBeanInfo();
967      }
968
969      if (childUrl == null)
970         return new JsonNode(n, parentKey, o2, ct2);
971
972      return getNode(childUrl, new JsonNode(n, parentKey, o2, ct2));
973   }
974}