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