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