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