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 w800'> 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 w800'> 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 <c>put("", xxx);</c> 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 w800'> 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 w800'> 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 * <c>Collection</c> classes are assumed to be followed by zero or one objects indicating the element type. 285 * 286 * <p> 287 * <c>Map</c> 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 * <ul class='notes'> 293 * <li> 294 * Use the {@link #get(String, Class)} method instead if you don't need a parameterized map/collection. 295 * </ul> 296 * 297 * @param url 298 * The URL of the element to retrieve. 299 * If <jk>null</jk> or blank, returns the root. 300 * @param type The specified object type. 301 * @param args The specified object parameter types. 302 * 303 * @param <T> The specified object type. 304 * @return The addressed element, or null if that element does not exist in the tree. 305 */ 306 public <T> T get(String url, Type type, Type...args) { 307 return getWithDefault(url, null, type, args); 308 } 309 310 /** 311 * Same as {@link #get(String, Class)} but returns a default value if the addressed element is null or non-existent. 312 * 313 * @param url 314 * The URL of the element to retrieve. 315 * If <jk>null</jk> or blank, returns the root. 316 * @param def The default value if addressed item does not exist. 317 * @param type The specified object type. 318 * 319 * @param <T> The specified object type. 320 * @return The addressed element, or null if that element does not exist in the tree. 321 */ 322 public <T> T getWithDefault(String url, T def, Class<T> type) { 323 Object o = service(GET, url, null); 324 if (o == null) 325 return def; 326 return session.convertToType(o, type); 327 } 328 329 /** 330 * Same as {@link #get(String,Type,Type[])} but returns a default value if the addressed element is null or non-existent. 331 * 332 * @param url 333 * The URL of the element to retrieve. 334 * If <jk>null</jk> or blank, returns the root. 335 * @param def The default value if addressed item does not exist. 336 * @param type The specified object type. 337 * @param args The specified object parameter types. 338 * 339 * @param <T> The specified object type. 340 * @return The addressed element, or null if that element does not exist in the tree. 341 */ 342 public <T> T getWithDefault(String url, T def, Type type, Type...args) { 343 Object o = service(GET, url, null); 344 if (o == null) 345 return def; 346 return session.convertToType(o, type, args); 347 } 348 349 /** 350 * Returns the specified entry value converted to a {@link String}. 351 * 352 * <p> 353 * Shortcut for <code>get(String.<jk>class</jk>, key)</code>. 354 * 355 * @param url The key. 356 * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key. 357 */ 358 public String getString(String url) { 359 return get(url, String.class); 360 } 361 362 /** 363 * Returns the specified entry value converted to a {@link String}. 364 * 365 * <p> 366 * Shortcut for <code>get(String.<jk>class</jk>, key, defVal)</code>. 367 * 368 * @param url The key. 369 * @param defVal The default value if the map doesn't contain the specified mapping. 370 * @return The converted value, or the default value if the map contains no mapping for this key. 371 */ 372 public String getString(String url, String defVal) { 373 return getWithDefault(url, defVal, String.class); 374 } 375 376 /** 377 * Returns the specified entry value converted to an {@link Integer}. 378 * 379 * <p> 380 * Shortcut for <code>get(Integer.<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 Integer getInt(String url) { 387 return get(url, Integer.class); 388 } 389 390 /** 391 * Returns the specified entry value converted to an {@link Integer}. 392 * 393 * <p> 394 * Shortcut for <code>get(Integer.<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 Integer getInt(String url, Integer defVal) { 402 return getWithDefault(url, defVal, Integer.class); 403 } 404 405 /** 406 * Returns the specified entry value converted to a {@link Long}. 407 * 408 * <p> 409 * Shortcut for <code>get(Long.<jk>class</jk>, key)</code>. 410 * 411 * @param url The key. 412 * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key. 413 * @throws InvalidDataConversionException If value cannot be converted. 414 */ 415 public Long getLong(String url) { 416 return get(url, Long.class); 417 } 418 419 /** 420 * Returns the specified entry value converted to a {@link Long}. 421 * 422 * <p> 423 * Shortcut for <code>get(Long.<jk>class</jk>, key, defVal)</code>. 424 * 425 * @param url The key. 426 * @param defVal The default value if the map doesn't contain the specified mapping. 427 * @return The converted value, or the default value if the map contains no mapping for this key. 428 * @throws InvalidDataConversionException If value cannot be converted. 429 */ 430 public Long getLong(String url, Long defVal) { 431 return getWithDefault(url, defVal, Long.class); 432 } 433 434 /** 435 * Returns the specified entry value converted to a {@link Boolean}. 436 * 437 * <p> 438 * Shortcut for <code>get(Boolean.<jk>class</jk>, key)</code>. 439 * 440 * @param url The key. 441 * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key. 442 * @throws InvalidDataConversionException If value cannot be converted. 443 */ 444 public Boolean getBoolean(String url) { 445 return get(url, Boolean.class); 446 } 447 448 /** 449 * Returns the specified entry value converted to a {@link Boolean}. 450 * 451 * <p> 452 * Shortcut for <code>get(Boolean.<jk>class</jk>, key, defVal)</code>. 453 * 454 * @param url The key. 455 * @param defVal The default value if the map doesn't contain the specified mapping. 456 * @return The converted value, or the default value if the map contains no mapping for this key. 457 * @throws InvalidDataConversionException If value cannot be converted. 458 */ 459 public Boolean getBoolean(String url, Boolean defVal) { 460 return getWithDefault(url, defVal, Boolean.class); 461 } 462 463 /** 464 * Returns the specified entry value converted to a {@link Map}. 465 * 466 * <p> 467 * Shortcut for <code>get(Map.<jk>class</jk>, key)</code>. 468 * 469 * @param url The key. 470 * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key. 471 * @throws InvalidDataConversionException If value cannot be converted. 472 */ 473 public Map<?,?> getMap(String url) { 474 return get(url, Map.class); 475 } 476 477 /** 478 * Returns the specified entry value converted to a {@link Map}. 479 * 480 * <p> 481 * Shortcut for <code>get(Map.<jk>class</jk>, key, defVal)</code>. 482 * 483 * @param url The key. 484 * @param defVal The default value if the map doesn't contain the specified mapping. 485 * @return The converted value, or the default value if the map contains no mapping for this key. 486 * @throws InvalidDataConversionException If value cannot be converted. 487 */ 488 public Map<?,?> getMap(String url, Map<?,?> defVal) { 489 return getWithDefault(url, defVal, Map.class); 490 } 491 492 /** 493 * Returns the specified entry value converted to a {@link List}. 494 * 495 * <p> 496 * Shortcut for <code>get(List.<jk>class</jk>, key)</code>. 497 * 498 * @param url The key. 499 * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key. 500 * @throws InvalidDataConversionException If value cannot be converted. 501 */ 502 public List<?> getList(String url) { 503 return get(url, List.class); 504 } 505 506 /** 507 * Returns the specified entry value converted to a {@link List}. 508 * 509 * <p> 510 * Shortcut for <code>get(List.<jk>class</jk>, key, defVal)</code>. 511 * 512 * @param url The key. 513 * @param defVal The default value if the map doesn't contain the specified mapping. 514 * @return The converted value, or the default value if the map contains no mapping for this key. 515 * @throws InvalidDataConversionException If value cannot be converted. 516 */ 517 public List<?> getList(String url, List<?> defVal) { 518 return getWithDefault(url, defVal, List.class); 519 } 520 521 /** 522 * Returns the specified entry value converted to a {@link Map}. 523 * 524 * <p> 525 * Shortcut for <code>get(ObjectMap.<jk>class</jk>, key)</code>. 526 * 527 * @param url The key. 528 * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key. 529 * @throws InvalidDataConversionException If value cannot be converted. 530 */ 531 public ObjectMap getObjectMap(String url) { 532 return get(url, ObjectMap.class); 533 } 534 535 /** 536 * Returns the specified entry value converted to a {@link ObjectMap}. 537 * 538 * <p> 539 * Shortcut for <code>get(ObjectMap.<jk>class</jk>, key, defVal)</code>. 540 * 541 * @param url The key. 542 * @param defVal The default value if the map doesn't contain the specified mapping. 543 * @return The converted value, or the default value if the map contains no mapping for this key. 544 * @throws InvalidDataConversionException If value cannot be converted. 545 */ 546 public ObjectMap getObjectMap(String url, ObjectMap defVal) { 547 return getWithDefault(url, defVal, ObjectMap.class); 548 } 549 550 /** 551 * Returns the specified entry value converted to a {@link ObjectList}. 552 * 553 * <p> 554 * Shortcut for <code>get(ObjectList.<jk>class</jk>, key)</code>. 555 * 556 * @param url The key. 557 * @return The converted value, or <jk>null</jk> if the map contains no mapping for this key. 558 * @throws InvalidDataConversionException If value cannot be converted. 559 */ 560 public ObjectList getObjectList(String url) { 561 return get(url, ObjectList.class); 562 } 563 564 /** 565 * Returns the specified entry value converted to a {@link ObjectList}. 566 * 567 * <p> 568 * Shortcut for <code>get(ObjectList.<jk>class</jk>, key, defVal)</code>. 569 * 570 * @param url The key. 571 * @param defVal The default value if the map doesn't contain the specified mapping. 572 * @return The converted value, or the default value if the map contains no mapping for this key. 573 * @throws InvalidDataConversionException If value cannot be converted. 574 */ 575 public ObjectList getObjectList(String url, ObjectList defVal) { 576 return getWithDefault(url, defVal, ObjectList.class); 577 } 578 579 /** 580 * Executes the specified method with the specified parameters on the specified object. 581 * 582 * @param url The URL of the element to retrieve. 583 * @param method 584 * The method signature. 585 * <p> 586 * Can be any of the following formats: 587 * <ul class='spaced-list'> 588 * <li> 589 * Method name only. e.g. <js>"myMethod"</js>. 590 * <li> 591 * Method name with class names. e.g. <js>"myMethod(String,int)"</js>. 592 * <li> 593 * Method name with fully-qualified class names. e.g. <js>"myMethod(java.util.String,int)"</js>. 594 * </ul> 595 * <p> 596 * As a rule, use the simplest format needed to uniquely resolve a method. 597 * @param args 598 * The arguments to pass as parameters to the method. 599 * These will automatically be converted to the appropriate object type if possible. 600 * This must be an array, like a JSON array. 601 * @return The returned object from the method call. 602 * @throws ExecutableException Exception occurred on invoked constructor/method/field. 603 * @throws ParseException Malformed input encountered. 604 * @throws IOException Thrown by underlying stream. 605 */ 606 public Object invokeMethod(String url, String method, String args) throws ExecutableException, ParseException, IOException { 607 try { 608 return new PojoIntrospector(get(url), parser).invokeMethod(method, args); 609 } catch (NoSuchMethodException | IllegalArgumentException | InvocationTargetException | IllegalAccessException e) { 610 throw new ExecutableException(e); 611 } 612 } 613 614 /** 615 * Returns the list of available methods that can be passed to the {@link #invokeMethod(String, String, String)} 616 * for the object addressed by the specified URL. 617 * 618 * @param url The URL. 619 * @return The list of methods. 620 */ 621 public Collection<String> getPublicMethods(String url) { 622 Object o = get(url); 623 if (o == null) 624 return null; 625 return session.getClassMeta(o.getClass()).getPublicMethods().keySet(); 626 } 627 628 /** 629 * Returns the class type of the object at the specified URL. 630 * 631 * @param url The URL. 632 * @return The class type. 633 */ 634 public ClassMeta getClassMeta(String url) { 635 JsonNode n = getNode(normalizeUrl(url), root); 636 if (n == null) 637 return null; 638 return n.cm; 639 } 640 641 /** 642 * Sets/replaces the element addressed by the URL. 643 * 644 * <p> 645 * This method expands the POJO model as necessary to create the new element. 646 * 647 * @param url 648 * The URL of the element to create. 649 * If <jk>null</jk> or blank, the root itself is replaced with the specified value. 650 * @param val The value being set. Value can be of any type. 651 * @return The previously addressed element, or <jk>null</jk> the element did not previously exist. 652 */ 653 public Object put(String url, Object val) { 654 return service(PUT, url, val); 655 } 656 657 /** 658 * Adds a value to a list element in a POJO model. 659 * 660 * <p> 661 * The URL is the address of the list being added to. 662 * 663 * <p> 664 * If the list does not already exist, it will be created. 665 * 666 * <p> 667 * This method expands the POJO model as necessary to create the new element. 668 * 669 * <ul class='notes'> 670 * <li> 671 * You can only post to three types of nodes: 672 * <ul> 673 * <li>{@link List Lists} 674 * <li>{@link Map Maps} containing integers as keys (i.e sparse arrays) 675 * <li>arrays 676 * </ul> 677 * </ul> 678 * 679 * @param url 680 * The URL of the element being added to. 681 * If <jk>null</jk> or blank, the root itself (assuming it's one of the types specified above) is added to. 682 * @param val The value being added. 683 * @return The URL of the element that was added. 684 */ 685 public String post(String url, Object val) { 686 return (String)service(POST, url, val); 687 } 688 689 /** 690 * Remove an element from a POJO model. 691 * 692 * <p> 693 * If the element does not exist, no action is taken. 694 * 695 * @param url 696 * The URL of the element being deleted. 697 * If <jk>null</jk> or blank, the root itself is deleted. 698 * @return The removed element, or null if that element does not exist. 699 */ 700 public Object delete(String url) { 701 return service(DELETE, url, null); 702 } 703 704 @Override /* Object */ 705 public String toString() { 706 return String.valueOf(root.o); 707 } 708 709 /** Handle nulls and strip off leading '/' char. */ 710 private static String normalizeUrl(String url) { 711 712 // Interpret nulls and blanks the same (i.e. as addressing the root itself) 713 if (url == null) 714 url = ""; 715 716 // Strip off leading slash if present. 717 if (url.length() > 0 && url.charAt(0) == '/') 718 url = url.substring(1); 719 720 return url; 721 } 722 723 724 /* 725 * Workhorse method. 726 */ 727 private Object service(int method, String url, Object val) throws PojoRestException { 728 729 url = normalizeUrl(url); 730 731 if (method == GET) { 732 JsonNode p = getNode(url, root); 733 return p == null ? null : p.o; 734 } 735 736 // Get the url of the parent and the property name of the addressed object. 737 int i = url.lastIndexOf('/'); 738 String parentUrl = (i == -1 ? null : url.substring(0, i)); 739 String childKey = (i == -1 ? url : url.substring(i + 1)); 740 741 if (method == PUT) { 742 if (url.length() == 0) { 743 if (rootLocked) 744 throw new PojoRestException(HTTP_FORBIDDEN, "Cannot overwrite root object"); 745 Object o = root.o; 746 root = new JsonNode(null, null, val, session.object()); 747 return o; 748 } 749 JsonNode n = (parentUrl == null ? root : getNode(parentUrl, root)); 750 if (n == null) 751 throw new PojoRestException(HTTP_NOT_FOUND, "Node at URL ''{0}'' not found.", parentUrl); 752 ClassMeta cm = n.cm; 753 Object o = n.o; 754 if (cm.isMap()) 755 return ((Map)o).put(childKey, convert(val, cm.getValueType())); 756 if (cm.isCollection() && o instanceof List) 757 return ((List)o).set(parseInt(childKey), convert(val, cm.getElementType())); 758 if (cm.isArray()) { 759 o = setArrayEntry(n.o, parseInt(childKey), val, cm.getElementType()); 760 ClassMeta pct = n.parent.cm; 761 Object po = n.parent.o; 762 if (pct.isMap()) { 763 ((Map)po).put(n.keyName, o); 764 return url; 765 } 766 if (pct.isBean()) { 767 BeanMap m = session.toBeanMap(po); 768 m.put(n.keyName, o); 769 return url; 770 } 771 throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform PUT on ''{0}'' with parent node type ''{1}''", url, pct); 772 } 773 if (cm.isBean()) 774 return session.toBeanMap(o).put(childKey, val); 775 throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform PUT on ''{0}'' whose parent is of type ''{1}''", url, cm); 776 } 777 778 if (method == POST) { 779 // Handle POST to root special 780 if (url.length() == 0) { 781 ClassMeta cm = root.cm; 782 Object o = root.o; 783 if (cm.isCollection()) { 784 Collection c = (Collection)o; 785 c.add(convert(val, cm.getElementType())); 786 return (c instanceof List ? url + "/" + (c.size()-1) : null); 787 } 788 if (cm.isArray()) { 789 Object[] o2 = addArrayEntry(o, val, cm.getElementType()); 790 root = new JsonNode(null, null, o2, null); 791 return url + "/" + (o2.length-1); 792 } 793 throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform POST on ''{0}'' of type ''{1}''", url, cm); 794 } 795 JsonNode n = getNode(url, root); 796 if (n == null) 797 throw new PojoRestException(HTTP_NOT_FOUND, "Node at URL ''{0}'' not found.", url); 798 ClassMeta cm = n.cm; 799 Object o = n.o; 800 if (cm.isArray()) { 801 Object[] o2 = addArrayEntry(o, val, cm.getElementType()); 802 ClassMeta pct = n.parent.cm; 803 Object po = n.parent.o; 804 if (pct.isMap()) { 805 ((Map)po).put(childKey, o2); 806 return url + "/" + (o2.length-1); 807 } 808 if (pct.isBean()) { 809 BeanMap m = session.toBeanMap(po); 810 m.put(childKey, o2); 811 return url + "/" + (o2.length-1); 812 } 813 throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform POST on ''{0}'' with parent node type ''{1}''", url, pct); 814 } 815 if (cm.isCollection()) { 816 Collection c = (Collection)o; 817 c.add(convert(val, cm.getElementType())); 818 return (c instanceof List ? url + "/" + (c.size()-1) : null); 819 } 820 throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform POST on ''{0}'' of type ''{1}''", url, cm); 821 } 822 823 if (method == DELETE) { 824 if (url.length() == 0) { 825 if (rootLocked) 826 throw new PojoRestException(HTTP_FORBIDDEN, "Cannot overwrite root object"); 827 Object o = root.o; 828 root = new JsonNode(null, null, null, session.object()); 829 return o; 830 } 831 JsonNode n = (parentUrl == null ? root : getNode(parentUrl, root)); 832 ClassMeta cm = n.cm; 833 Object o = n.o; 834 if (cm.isMap()) 835 return ((Map)o).remove(childKey); 836 if (cm.isCollection() && o instanceof List) 837 return ((List)o).remove(parseInt(childKey)); 838 if (cm.isArray()) { 839 int index = parseInt(childKey); 840 Object old = ((Object[])o)[index]; 841 Object[] o2 = removeArrayEntry(o, index); 842 ClassMeta pct = n.parent.cm; 843 Object po = n.parent.o; 844 if (pct.isMap()) { 845 ((Map)po).put(n.keyName, o2); 846 return old; 847 } 848 if (pct.isBean()) { 849 BeanMap m = session.toBeanMap(po); 850 m.put(n.keyName, o2); 851 return old; 852 } 853 throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform POST on ''{0}'' with parent node type ''{1}''", url, pct); 854 } 855 if (cm.isBean()) 856 return session.toBeanMap(o).put(childKey, null); 857 throw new PojoRestException(HTTP_BAD_REQUEST, "Cannot perform PUT on ''{0}'' whose parent is of type ''{1}''", url, cm); 858 } 859 860 return null; // Never gets here. 861 } 862 863 private Object[] setArrayEntry(Object o, int index, Object val, ClassMeta componentType) { 864 Object[] a = (Object[])o; 865 if (a.length <= index) { 866 // Expand out the array. 867 Object[] a2 = (Object[])Array.newInstance(a.getClass().getComponentType(), index+1); 868 System.arraycopy(a, 0, a2, 0, a.length); 869 a = a2; 870 } 871 a[index] = convert(val, componentType); 872 return a; 873 } 874 875 private Object[] addArrayEntry(Object o, Object val, ClassMeta componentType) { 876 Object[] a = (Object[])o; 877 // Expand out the array. 878 Object[] a2 = (Object[])Array.newInstance(a.getClass().getComponentType(), a.length+1); 879 System.arraycopy(a, 0, a2, 0, a.length); 880 a2[a.length] = convert(val, componentType); 881 return a2; 882 } 883 884 private static Object[] removeArrayEntry(Object o, int index) { 885 Object[] a = (Object[])o; 886 // Shrink the array. 887 Object[] a2 = (Object[])Array.newInstance(a.getClass().getComponentType(), a.length-1); 888 System.arraycopy(a, 0, a2, 0, index); 889 System.arraycopy(a, index+1, a2, index, a.length-index-1); 890 return a2; 891 } 892 893 class JsonNode { 894 Object o; 895 ClassMeta cm; 896 JsonNode parent; 897 String keyName; 898 899 JsonNode(JsonNode parent, String keyName, Object o, ClassMeta cm) { 900 this.o = o; 901 this.keyName = keyName; 902 this.parent = parent; 903 if (cm == null || cm.isObject()) { 904 if (o == null) 905 cm = session.object(); 906 else 907 cm = session.getClassMetaForObject(o); 908 } 909 this.cm = cm; 910 } 911 } 912 913 JsonNode getNode(String url, JsonNode n) { 914 if (url == null || url.isEmpty()) 915 return n; 916 int i = url.indexOf('/'); 917 String parentKey, childUrl = null; 918 if (i == -1) { 919 parentKey = url; 920 } else { 921 parentKey = url.substring(0, i); 922 childUrl = url.substring(i + 1); 923 } 924 925 Object o = n.o; 926 Object o2 = null; 927 ClassMeta cm = n.cm; 928 ClassMeta ct2 = null; 929 if (o == null) 930 return null; 931 if (cm.isMap()) { 932 o2 = ((Map)o).get(parentKey); 933 ct2 = cm.getValueType(); 934 } else if (cm.isCollection() && o instanceof List) { 935 int key = parseInt(parentKey); 936 List l = ((List)o); 937 if (l.size() <= key) 938 return null; 939 o2 = l.get(key); 940 ct2 = cm.getElementType(); 941 } else if (cm.isArray()) { 942 int key = parseInt(parentKey); 943 Object[] a = ((Object[])o); 944 if (a.length <= key) 945 return null; 946 o2 = a[key]; 947 ct2 = cm.getElementType(); 948 } else if (cm.isBean()) { 949 BeanMap m = session.toBeanMap(o); 950 o2 = m.get(parentKey); 951 BeanPropertyMeta pMeta = m.getPropertyMeta(parentKey); 952 if (pMeta == null) 953 throw new PojoRestException(HTTP_BAD_REQUEST, 954 "Unknown property ''{0}'' encountered while trying to parse into class ''{1}''", 955 parentKey, m.getClassMeta() 956 ); 957 ct2 = pMeta.getClassMeta(); 958 } 959 960 if (childUrl == null) 961 return new JsonNode(n, parentKey, o2, ct2); 962 963 return getNode(childUrl, new JsonNode(n, parentKey, o2, ct2)); 964 } 965 966 private Object convert(Object in, ClassMeta cm) { 967 if (cm == null) 968 return in; 969 if (cm.isBean() && in instanceof Map) 970 return session.convertToType(in, cm); 971 return in; 972 } 973 974 private static int parseInt(String key) { 975 try { 976 return Integer.parseInt(key); 977 } catch (NumberFormatException e) { 978 throw new PojoRestException(HTTP_BAD_REQUEST, 979 "Cannot address an item in an array with a non-integer key ''{0}''", key 980 ); 981 } 982 } 983}