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