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