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