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':' > 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<String> <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<MyBean> <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<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>); 313 * 314 * <jc>// Value converted to a map of string keys/values.</jc> 315 * 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>); 316 * 317 * <jc>// Value converted to a map containing string keys and values of lists containing beans.</jc> 318 * 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>); 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}