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