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