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.json.*; 023import org.apache.juneau.parser.*; 024 025/** 026 * Provides the ability to perform standard REST operations (GET, PUT, POST, DELETE) against nodes in a POJO model. 027 * 028 * <p> 029 * Nodes in the POJO model are addressed using URLs. 030 * 031 * <p> 032 * A POJO model is defined as a tree model where nodes consist of consisting of the following: 033 * <ul class='spaced-list'> 034 * <li> 035 * {@link Map Maps} and Java beans representing JSON objects. 036 * <li> 037 * {@link Collection Collections} and arrays representing JSON arrays. 038 * <li> 039 * Java beans. 040 * </ul> 041 * 042 * <p> 043 * Leaves of the tree can be any type of object. 044 * 045 * <p> 046 * Use {@link #get(String) get()} to retrieve an element from a JSON tree. 047 * <br>Use {@link #put(String,Object) put()} to create (or overwrite) an element in a JSON tree. 048 * <br>Use {@link #post(String,Object) post()} to add an element to a list in a JSON tree. 049 * <br>Use {@link #delete(String) delete()} to remove an element from a JSON tree. 050 * 051 * <p> 052 * Leading slashes in URLs are ignored. 053 * So <js>"/xxx/yyy/zzz"</js> and <js>"xxx/yyy/zzz"</js> are considered identical. 054 * 055 * <h5 class='section'>Example:</h5> 056 * <p class='bcode'> 057 * <jc>// Construct an unstructured POJO model</jc> 058 * ObjectMap m = <jk>new</jk> ObjectMap(<js>""</js> 059 * + <js>"{"</js> 060 * + <js>" name:'John Smith', "</js> 061 * + <js>" address:{ "</js> 062 * + <js>" streetAddress:'21 2nd Street', "</js> 063 * + <js>" city:'New York', "</js> 064 * + <js>" state:'NY', "</js> 065 * + <js>" postalCode:10021 "</js> 066 * + <js>" }, "</js> 067 * + <js>" phoneNumbers:[ "</js> 068 * + <js>" '212 555-1111', "</js> 069 * + <js>" '212 555-2222' "</js> 070 * + <js>" ], "</js> 071 * + <js>" additionalInfo:null, "</js> 072 * + <js>" remote:false, "</js> 073 * + <js>" height:62.4, "</js> 074 * + <js>" 'fico score':' > 640' "</js> 075 * + <js>"} "</js> 076 * ); 077 * 078 * <jc>// Wrap Map inside a PojoRest object</jc> 079 * PojoRest johnSmith = <jk>new</jk> PojoRest(m); 080 * 081 * <jc>// Get a simple value at the top level</jc> 082 * <jc>// "John Smith"</jc> 083 * String name = johnSmith.getString(<js>"name"</js>); 084 * 085 * <jc>// Change a simple value at the top level</jc> 086 * johnSmith.put(<js>"name"</js>, <js>"The late John Smith"</js>); 087 * 088 * <jc>// Get a simple value at a deep level</jc> 089 * <jc>// "21 2nd Street"</jc> 090 * String streetAddress = johnSmith.getString(<js>"address/streetAddress"</js>); 091 * 092 * <jc>// Set a simple value at a deep level</jc> 093 * johnSmith.put(<js>"address/streetAddress"</js>, <js>"101 Cemetery Way"</js>); 094 * 095 * <jc>// Get entries in a list</jc> 096 * <jc>// "212 555-1111"</jc> 097 * String firstPhoneNumber = johnSmith.getString(<js>"phoneNumbers/0"</js>); 098 * 099 * <jc>// Add entries to a list</jc> 100 * johnSmith.post(<js>"phoneNumbers"</js>, <js>"212 555-3333"</js>); 101 * 102 * <jc>// Delete entries from a model</jc> 103 * johnSmith.delete(<js>"fico score"</js>); 104 * 105 * <jc>// Add entirely new structures to the tree</jc> 106 * ObjectMap medicalInfo = new ObjectMap(<js>""</js> 107 * + <js>"{"</js> 108 * + <js>" currentStatus: 'deceased',"</js> 109 * + <js>" health: 'non-existent',"</js> 110 * + <js>" creditWorthiness: 'not good'"</js> 111 * + <js>"}"</js> 112 * ); 113 * johnSmith.put(<js>"additionalInfo/medicalInfo"</js>, medicalInfo); 114 * </p> 115 * 116 * <p> 117 * In the special case of collections/arrays of maps/beans, a special XPath-like selector notation can be used in lieu 118 * of index numbers on GET requests to return a map/bean with a specified attribute value. 119 * <br>The syntax is {@code @attr=val}, where attr is the attribute name on the child map, and val is the matching value. 120 * 121 * <h5 class='section'>Example:</h5> 122 * <p class='bcode'> 123 * <jc>// Get map/bean with name attribute value of 'foo' from a list of items</jc> 124 * Map m = pojoRest.getMap(<js>"/items/@name=foo"</js>); 125 * </p> 126 */ 127@SuppressWarnings({"unchecked","rawtypes"}) 128public final class PojoRest { 129 130 /** The list of possible request types. */ 131 private static final int GET=1, PUT=2, POST=3, DELETE=4; 132 133 private ReaderParser parser = JsonParser.DEFAULT; 134 final BeanSession session; 135 136 /** If true, the root cannot be overwritten */ 137 private boolean rootLocked = false; 138 139 /** The root of the model. */ 140 private JsonNode root; 141 142 /** 143 * Create a new instance of a REST interface over the specified object. 144 * 145 * <p> 146 * Uses {@link BeanContext#DEFAULT} for working with Java beans. 147 * 148 * @param o The object to be wrapped. 149 */ 150 public PojoRest(Object o) { 151 this(o, null); 152 } 153 154 /** 155 * Create a new instance of a REST interface over the specified object. 156 * 157 * <p> 158 * The parser is used as the bean context. 159 * 160 * @param o The object to be wrapped. 161 * @param parser The parser to use for parsing arguments and converting objects to the correct data type. 162 */ 163 public PojoRest(Object o, ReaderParser parser) { 164 if (parser == null) 165 parser = JsonParser.DEFAULT; 166 this.parser = parser; 167 this.session = parser.createBeanSession(); 168 this.root = new JsonNode(null, null, o, session.object()); 169 } 170 171 /** 172 * Call this method to prevent the root object from being overwritten on <code>put("", xxx);</code> calls. 173 * 174 * @return This object (for method chaining). 175 */ 176 public PojoRest setRootLocked() { 177 this.rootLocked = true; 178 return this; 179 } 180 181 /** 182 * The root object that was passed into the constructor of this method. 183 * 184 * @return The root object. 185 */ 186 public Object getRootObject() { 187 return root.o; 188 } 189 190 /** 191 * Retrieves the element addressed by the URL. 192 * 193 * @param url 194 * The URL of the element to retrieve. 195 * <br>If <jk>null</jk> or blank, returns the root. 196 * @return The addressed element, or <jk>null</jk> if that element does not exist in the tree. 197 */ 198 public Object get(String url) { 199 return getWithDefault(url, null); 200 } 201 202 /** 203 * Retrieves the element addressed by the URL. 204 * 205 * @param url 206 * The URL of the element to retrieve. 207 * <br>If <jk>null</jk> or blank, returns the root. 208 * @param defVal The default value if the map doesn't contain the specified mapping. 209 * @return The addressed element, or null if that element does not exist in the tree. 210 */ 211 public Object getWithDefault(String url, Object defVal) { 212 Object o = service(GET, url, null); 213 return o == null ? defVal : o; 214 } 215 216 /** 217 * Retrieves the element addressed by the URL as the specified object type. 218 * 219 * <p> 220 * Will convert object to the specified type per {@link BeanSession#convertToType(Object, Class)}. 221 * 222 * <h5 class='section'>Examples:</h5> 223 * <p class='bcode'> 224 * PojoRest r = <jk>new</jk> PojoRest(object); 225 * 226 * <jc>// Value converted to a string.</jc> 227 * String s = r.get(<js>"path/to/string"</js>, String.<jk>class</jk>); 228 * 229 * <jc>// Value converted to a bean.</jc> 230 * MyBean b = r.get(<js>"path/to/bean"</js>, MyBean.<jk>class</jk>); 231 * 232 * <jc>// Value converted to a bean array.</jc> 233 * MyBean[] ba = r.get(<js>"path/to/beanarray"</js>, MyBean[].<jk>class</jk>); 234 * 235 * <jc>// Value converted to a linked-list of objects.</jc> 236 * List l = r.get(<js>"path/to/list"</js>, LinkedList.<jk>class</jk>); 237 * 238 * <jc>// Value converted to a map of object keys/values.</jc> 239 * Map m2 = r.get(<js>"path/to/map"</js>, TreeMap.<jk>class</jk>); 240 * </p> 241 * 242 * @param url 243 * The URL of the element to retrieve. 244 * If <jk>null</jk> or blank, returns the root. 245 * @param type The specified object type. 246 * 247 * @param <T> The specified object type. 248 * @return The addressed element, or null if that element does not exist in the tree. 249 */ 250 public <T> T get(String url, Class<T> type) { 251 return getWithDefault(url, null, type); 252 } 253 254 /** 255 * Retrieves the element addressed by the URL as the specified object type. 256 * 257 * <p> 258 * Will convert object to the specified type per {@link BeanSession#convertToType(Object, Class)}. 259 * 260 * <p> 261 * The type can be a simple type (e.g. beans, strings, numbers) or parameterized type (collections/maps). 262 * 263 * <h5 class='section'>Examples:</h5> 264 * <p class='bcode'> 265 * PojoMap r = <jk>new</jk> PojoMap(object); 266 * 267 * <jc>// Value converted to a linked-list of strings.</jc> 268 * List<String> l1 = r.get(<js>"path/to/list1"</js>, LinkedList.<jk>class</jk>, String.<jk>class</jk>); 269 * 270 * <jc>// Value converted to a linked-list of beans.</jc> 271 * List<MyBean> l2 = r.get(<js>"path/to/list2"</js>, LinkedList.<jk>class</jk>, MyBean.<jk>class</jk>); 272 * 273 * <jc>// Value converted to a linked-list of linked-lists of strings.</jc> 274 * List<List<String>> l3 = r.get(<js>"path/to/list3"</js>, LinkedList.<jk>class</jk>, LinkedList.<jk>class</jk>, String.<jk>class</jk>); 275 * 276 * <jc>// Value converted to a map of string keys/values.</jc> 277 * Map<String,String> m1 = r.get(<js>"path/to/map1"</js>, TreeMap.<jk>class</jk>, String.<jk>class</jk>, String.<jk>class</jk>); 278 * 279 * <jc>// Value converted to a map containing string keys and values of lists containing beans.</jc> 280 * 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>); 281 * </p> 282 * 283 * <p> 284 * <code>Collection</code> classes are assumed to be followed by zero or one objects indicating the element type. 285 * 286 * <p> 287 * <code>Map</code> classes are assumed to be followed by zero or two meta objects indicating the key and value types. 288 * 289 * <p> 290 * The array can be arbitrarily long to indicate arbitrarily complex data structures. 291 * 292 * <h5 class='section'>Notes:</h5> 293 * <ul class='spaced-list'> 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 * <p> 526 * Shortcut for <code>get(ObjectMap.<jk>class</jk>, key)</code>. 527 * 528 * @param url The key. 529 * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key. 530 * @throws InvalidDataConversionException If value cannot be converted. 531 */ 532 public ObjectMap getObjectMap(String url) { 533 return get(url, ObjectMap.class); 534 } 535 536 /** 537 * Returns the specified entry value converted to a {@link ObjectMap}. 538 * 539 * <p> 540 * Shortcut for <code>get(ObjectMap.<jk>class</jk>, key, defVal)</code>. 541 * 542 * @param url The key. 543 * @param defVal The default value if the map doesn't contain the specified mapping. 544 * @return The converted value, or the default value if the map contains no mapping for this key. 545 * @throws InvalidDataConversionException If value cannot be converted. 546 */ 547 public ObjectMap getObjectMap(String url, ObjectMap defVal) { 548 return getWithDefault(url, defVal, ObjectMap.class); 549 } 550 551 /** 552 * Returns the specified entry value converted to a {@link ObjectList}. 553 * 554 * <p> 555 * Shortcut for <code>get(ObjectList.<jk>class</jk>, key)</code>. 556 * 557 * @param url The key. 558 * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key. 559 * @throws InvalidDataConversionException If value cannot be converted. 560 */ 561 public ObjectList getObjectList(String url) { 562 return get(url, ObjectList.class); 563 } 564 565 /** 566 * Returns the specified entry value converted to a {@link ObjectList}. 567 * 568 * <p> 569 * Shortcut for <code>get(ObjectList.<jk>class</jk>, key, defVal)</code>. 570 * 571 * @param url The key. 572 * @param defVal The default value if the map doesn't contain the specified mapping. 573 * @return The converted value, or the default value if the map contains no mapping for this key. 574 * @throws InvalidDataConversionException If value cannot be converted. 575 */ 576 public ObjectList getObjectList(String url, ObjectList defVal) { 577 return getWithDefault(url, defVal, ObjectList.class); 578 } 579 580 /** 581 * Executes the specified method with the specified parameters on the specified object. 582 * 583 * @param url The URL of the element to retrieve. 584 * @param method 585 * The method signature. 586 * <p> 587 * Can be any of the following formats: 588 * <ul class='spaced-list'> 589 * <li> 590 * Method name only. e.g. <js>"myMethod"</js>. 591 * <li> 592 * Method name with class names. e.g. <js>"myMethod(String,int)"</js>. 593 * <li> 594 * Method name with fully-qualified class names. e.g. <js>"myMethod(java.util.String,int)"</js>. 595 * </ul> 596 * <p> 597 * As a rule, use the simplest format needed to uniquely resolve a method. 598 * @param args 599 * The arguments to pass as parameters to the method. 600 * These will automatically be converted to the appropriate object type if possible. 601 * This must be an array, like a JSON array. 602 * @return The returned object from the method call. 603 * @throws IllegalAccessException 604 * If the <code>Constructor</code> object enforces Java language access control and the underlying constructor is 605 * inaccessible. 606 * @throws IllegalArgumentException 607 * If one of the following occurs: 608 * <ul class='spaced-list'> 609 * <li> 610 * The number of actual and formal parameters differ. 611 * <li> 612 * An unwrapping conversion for primitive arguments fails. 613 * <li> 614 * A parameter value cannot be converted to the corresponding formal parameter type by a method invocation 615 * conversion. 616 * <li> 617 * The constructor pertains to an enum type. 618 * </ul> 619 * @throws InvocationTargetException If the underlying constructor throws an exception. 620 * @throws ParseException If the input contains a syntax error or is malformed. 621 * @throws NoSuchMethodException 622 * @throws IOException 623 */ 624 public Object invokeMethod(String url, String method, String args) throws InvocationTargetException, 625 IllegalArgumentException, IllegalAccessException, ParseException, NoSuchMethodException, IOException { 626 return new PojoIntrospector(get(url), parser).invokeMethod(method, args); 627 } 628 629 /** 630 * Returns the list of available methods that can be passed to the {@link #invokeMethod(String, String, String)} 631 * for the object addressed by the specified URL. 632 * 633 * @param url The URL. 634 * @return The list of methods. 635 */ 636 public Collection<String> getPublicMethods(String url) { 637 Object o = get(url); 638 if (o == null) 639 return null; 640 return session.getClassMeta(o.getClass()).getPublicMethods().keySet(); 641 } 642 643 /** 644 * Returns the class type of the object at the specified URL. 645 * 646 * @param url The URL. 647 * @return The class type. 648 */ 649 public ClassMeta getClassMeta(String url) { 650 JsonNode n = getNode(normalizeUrl(url), root); 651 if (n == null) 652 return null; 653 return n.cm; 654 } 655 656 /** 657 * Sets/replaces the element addressed by the URL. 658 * 659 * <p> 660 * This method expands the POJO model as necessary to create the new element. 661 * 662 * @param url 663 * The URL of the element to create. 664 * If <jk>null</jk> or blank, the root itself is replaced with the specified value. 665 * @param val The value being set. Value can be of any type. 666 * @return The previously addressed element, or <jk>null</jk> the element did not previously exist. 667 */ 668 public Object put(String url, Object val) { 669 return service(PUT, url, val); 670 } 671 672 /** 673 * Adds a value to a list element in a POJO model. 674 * 675 * <p> 676 * The URL is the address of the list being added to. 677 * 678 * <p> 679 * If the list does not already exist, it will be created. 680 * 681 * <p> 682 * This method expands the POJO model as necessary to create the new element. 683 * 684 * <h5 class='section'>Notes:</h5> 685 * <ul class='spaced-list'> 686 * <li> 687 * You can only post to three types of nodes: 688 * <ul> 689 * <li>{@link List Lists} 690 * <li>{@link Map Maps} containing integers as keys (i.e sparse arrays) 691 * <li>arrays 692 * </ul> 693 * </ul> 694 * 695 * @param url 696 * The URL of the element being added to. 697 * If <jk>null</jk> or blank, the root itself (assuming it's one of the types specified above) is added to. 698 * @param val The value being added. 699 * @return The URL of the element that was added. 700 */ 701 public String post(String url, Object val) { 702 return (String)service(POST, url, val); 703 } 704 705 /** 706 * Remove an element from a POJO model. 707 * 708 * <p> 709 * If the element does not exist, no action is taken. 710 * 711 * @param url 712 * The URL of the element being deleted. 713 * If <jk>null</jk> or blank, the root itself is deleted. 714 * @return The removed element, or null if that element does not exist. 715 */ 716 public Object delete(String url) { 717 return service(DELETE, url, null); 718 } 719 720 @Override /* Object */ 721 public String toString() { 722 return String.valueOf(root.o); 723 } 724 725 /** Handle nulls and strip off leading '/' char. */ 726 private static String normalizeUrl(String url) { 727 728 // Interpret nulls and blanks the same (i.e. as addressing the root itself) 729 if (url == null) 730 url = ""; 731 732 // Strip off leading slash if present. 733 if (url.length() > 0 && url.charAt(0) == '/') 734 url = url.substring(1); 735 736 return url; 737 } 738 739 740 /* 741 * Workhorse method. 742 */ 743 private Object service(int method, String url, Object val) throws PojoRestException { 744 745 url = normalizeUrl(url); 746 747 if (method == GET) { 748 JsonNode p = getNode(url, root); 749 return p == null ? null : p.o; 750 } 751 752 // Get the url of the parent and the property name of the addressed object. 753 int i = url.lastIndexOf('/'); 754 String parentUrl = (i == -1 ? null : url.substring(0, i)); 755 String childKey = (i == -1 ? url : url.substring(i + 1)); 756 757 if (method == PUT) { 758 if (url.length() == 0) { 759 if (rootLocked) 760 throw new PojoRestException(HTTP_FORBIDDEN, "Cannot overwrite root object"); 761 Object o = root.o; 762 root = new JsonNode(null, null, val, session.object()); 763 return o; 764 } 765 JsonNode n = (parentUrl == null ? root : getNode(parentUrl, root)); 766 if (n == null) 767 throw new PojoRestException(HTTP_NOT_FOUND, "Node at URL ''{0}'' not found.", parentUrl); 768 ClassMeta cm = n.cm; 769 Object o = n.o; 770 if (cm.isMap()) 771 return ((Map)o).put(childKey, convert(val, cm.getValueType())); 772 if (cm.isCollection() && o instanceof List) 773 return ((List)o).set(parseInt(childKey), convert(val, cm.getElementType())); 774 if (cm.isArray()) { 775 o = setArrayEntry(n.o, parseInt(childKey), val, cm.getElementType()); 776 ClassMeta pct = n.parent.cm; 777 Object po = n.parent.o; 778 if (pct.isMap()) { 779 ((Map)po).put(n.keyName, o); 780 return url; 781 } 782 if (pct.isBean()) { 783 BeanMap m = session.toBeanMap(po); 784 m.put(n.keyName, o); 785 return url; 786 } 787 throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform PUT on ''{0}'' with parent node type ''{1}''", url, pct); 788 } 789 if (cm.isBean()) 790 return session.toBeanMap(o).put(childKey, val); 791 throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform PUT on ''{0}'' whose parent is of type ''{1}''", url, cm); 792 } 793 794 if (method == POST) { 795 // Handle POST to root special 796 if (url.length() == 0) { 797 ClassMeta cm = root.cm; 798 Object o = root.o; 799 if (cm.isCollection()) { 800 Collection c = (Collection)o; 801 c.add(convert(val, cm.getElementType())); 802 return (c instanceof List ? url + "/" + (c.size()-1) : null); 803 } 804 if (cm.isArray()) { 805 Object[] o2 = addArrayEntry(o, val, cm.getElementType()); 806 root = new JsonNode(null, null, o2, null); 807 return url + "/" + (o2.length-1); 808 } 809 throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform POST on ''{0}'' of type ''{1}''", url, cm); 810 } 811 JsonNode n = getNode(url, root); 812 if (n == null) 813 throw new PojoRestException(HTTP_NOT_FOUND, "Node at URL ''{0}'' not found.", url); 814 ClassMeta cm = n.cm; 815 Object o = n.o; 816 if (cm.isArray()) { 817 Object[] o2 = addArrayEntry(o, val, cm.getElementType()); 818 ClassMeta pct = n.parent.cm; 819 Object po = n.parent.o; 820 if (pct.isMap()) { 821 ((Map)po).put(childKey, o2); 822 return url + "/" + (o2.length-1); 823 } 824 if (pct.isBean()) { 825 BeanMap m = session.toBeanMap(po); 826 m.put(childKey, o2); 827 return url + "/" + (o2.length-1); 828 } 829 throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform POST on ''{0}'' with parent node type ''{1}''", url, pct); 830 } 831 if (cm.isCollection()) { 832 Collection c = (Collection)o; 833 c.add(convert(val, cm.getElementType())); 834 return (c instanceof List ? url + "/" + (c.size()-1) : null); 835 } 836 throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform POST on ''{0}'' of type ''{1}''", url, cm); 837 } 838 839 if (method == DELETE) { 840 if (url.length() == 0) { 841 if (rootLocked) 842 throw new PojoRestException(HTTP_FORBIDDEN, "Cannot overwrite root object"); 843 Object o = root.o; 844 root = new JsonNode(null, null, null, session.object()); 845 return o; 846 } 847 JsonNode n = (parentUrl == null ? root : getNode(parentUrl, root)); 848 ClassMeta cm = n.cm; 849 Object o = n.o; 850 if (cm.isMap()) 851 return ((Map)o).remove(childKey); 852 if (cm.isCollection() && o instanceof List) 853 return ((List)o).remove(parseInt(childKey)); 854 if (cm.isArray()) { 855 int index = parseInt(childKey); 856 Object old = ((Object[])o)[index]; 857 Object[] o2 = removeArrayEntry(o, index); 858 ClassMeta pct = n.parent.cm; 859 Object po = n.parent.o; 860 if (pct.isMap()) { 861 ((Map)po).put(n.keyName, o2); 862 return old; 863 } 864 if (pct.isBean()) { 865 BeanMap m = session.toBeanMap(po); 866 m.put(n.keyName, o2); 867 return old; 868 } 869 throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform POST on ''{0}'' with parent node type ''{1}''", url, pct); 870 } 871 if (cm.isBean()) 872 return session.toBeanMap(o).put(childKey, null); 873 throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform PUT on ''{0}'' whose parent is of type ''{1}''", url, cm); 874 } 875 876 return null; // Never gets here. 877 } 878 879 private Object[] setArrayEntry(Object o, int index, Object val, ClassMeta componentType) { 880 Object[] a = (Object[])o; 881 if (a.length <= index) { 882 // Expand out the array. 883 Object[] a2 = (Object[])Array.newInstance(a.getClass().getComponentType(), index+1); 884 System.arraycopy(a, 0, a2, 0, a.length); 885 a = a2; 886 } 887 a[index] = convert(val, componentType); 888 return a; 889 } 890 891 private Object[] addArrayEntry(Object o, Object val, ClassMeta componentType) { 892 Object[] a = (Object[])o; 893 // Expand out the array. 894 Object[] a2 = (Object[])Array.newInstance(a.getClass().getComponentType(), a.length+1); 895 System.arraycopy(a, 0, a2, 0, a.length); 896 a2[a.length] = convert(val, componentType); 897 return a2; 898 } 899 900 private static Object[] removeArrayEntry(Object o, int index) { 901 Object[] a = (Object[])o; 902 // Shrink the array. 903 Object[] a2 = (Object[])Array.newInstance(a.getClass().getComponentType(), a.length-1); 904 System.arraycopy(a, 0, a2, 0, index); 905 System.arraycopy(a, index+1, a2, index, a.length-index-1); 906 return a2; 907 } 908 909 class JsonNode { 910 Object o; 911 ClassMeta cm; 912 JsonNode parent; 913 String keyName; 914 915 JsonNode(JsonNode parent, String keyName, Object o, ClassMeta cm) { 916 this.o = o; 917 this.keyName = keyName; 918 this.parent = parent; 919 if (cm == null || cm.isObject()) { 920 if (o == null) 921 cm = session.object(); 922 else 923 cm = session.getClassMetaForObject(o); 924 } 925 this.cm = cm; 926 } 927 } 928 929 JsonNode getNode(String url, JsonNode n) { 930 if (url == null || url.isEmpty()) 931 return n; 932 int i = url.indexOf('/'); 933 String parentKey, childUrl = null; 934 if (i == -1) { 935 parentKey = url; 936 } else { 937 parentKey = url.substring(0, i); 938 childUrl = url.substring(i + 1); 939 } 940 941 Object o = n.o; 942 Object o2 = null; 943 ClassMeta cm = n.cm; 944 ClassMeta ct2 = null; 945 if (o == null) 946 return null; 947 if (cm.isMap()) { 948 o2 = ((Map)o).get(parentKey); 949 ct2 = cm.getValueType(); 950 } else if (cm.isCollection() && o instanceof List) { 951 int key = parseInt(parentKey); 952 List l = ((List)o); 953 if (l.size() <= key) 954 return null; 955 o2 = l.get(key); 956 ct2 = cm.getElementType(); 957 } else if (cm.isArray()) { 958 int key = parseInt(parentKey); 959 Object[] a = ((Object[])o); 960 if (a.length <= key) 961 return null; 962 o2 = a[key]; 963 ct2 = cm.getElementType(); 964 } else if (cm.isBean()) { 965 BeanMap m = session.toBeanMap(o); 966 o2 = m.get(parentKey); 967 BeanPropertyMeta pMeta = m.getPropertyMeta(parentKey); 968 if (pMeta == null) 969 throw new PojoRestException(HTTP_BAD_REQUEST, 970 "Unknown property ''{0}'' encountered while trying to parse into class ''{1}''", 971 parentKey, m.getClassMeta() 972 ); 973 ct2 = pMeta.getClassMeta(); 974 } 975 976 if (childUrl == null) 977 return new JsonNode(n, parentKey, o2, ct2); 978 979 return getNode(childUrl, new JsonNode(n, parentKey, o2, ct2)); 980 } 981 982 private Object convert(Object in, ClassMeta cm) { 983 if (cm == null) 984 return in; 985 if (cm.isBean() && in instanceof Map) 986 return session.convertToType(in, cm); 987 return in; 988 } 989 990 private static int parseInt(String key) { 991 try { 992 return Integer.parseInt(key); 993 } catch (NumberFormatException e) { 994 throw new PojoRestException(HTTP_BAD_REQUEST, 995 "Cannot address an item in an array with a non-integer key ''{0}''", key 996 ); 997 } 998 } 999}