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