001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.isis.viewer.restfulobjects.applib; 020 021import java.io.InputStream; 022import java.lang.reflect.Constructor; 023import java.math.BigDecimal; 024import java.math.BigInteger; 025import java.util.Arrays; 026import java.util.Collections; 027import java.util.Date; 028import java.util.Iterator; 029import java.util.List; 030import java.util.Map; 031import java.util.Map.Entry; 032 033import org.apache.isis.viewer.restfulobjects.applib.util.JsonNodeUtils; 034import org.apache.isis.viewer.restfulobjects.applib.util.PathNode; 035import org.apache.isis.viewer.restfulobjects.applib.util.UrlEncodingUtils; 036import org.codehaus.jackson.JsonNode; 037import org.codehaus.jackson.node.ArrayNode; 038import org.codehaus.jackson.node.JsonNodeFactory; 039import org.codehaus.jackson.node.NullNode; 040import org.codehaus.jackson.node.ObjectNode; 041import org.codehaus.jackson.node.POJONode; 042import org.joda.time.format.DateTimeFormat; 043import org.joda.time.format.DateTimeFormatter; 044 045import com.google.common.base.Function; 046import com.google.common.base.Predicate; 047import com.google.common.collect.Iterables; 048import com.google.common.collect.Iterators; 049import com.google.common.collect.Lists; 050import com.google.common.collect.Maps; 051 052/** 053 * A wrapper around {@link JsonNode} that provides some additional helper 054 * methods. 055 */ 056public class JsonRepresentation { 057 058 public interface HasLinkToSelf { 059 public LinkRepresentation getSelf(); 060 } 061 062 public interface HasLinkToUp { 063 public LinkRepresentation getUp(); 064 } 065 066 public interface HasLinks { 067 public JsonRepresentation getLinks(); 068 } 069 070 public interface HasExtensions { 071 public JsonRepresentation getExtensions(); 072 } 073 074 private static Map<Class<?>, Function<JsonNode, ?>> REPRESENTATION_INSTANTIATORS = Maps.newHashMap(); 075 static { 076 REPRESENTATION_INSTANTIATORS.put(String.class, new Function<JsonNode, String>() { 077 @Override 078 public String apply(final JsonNode input) { 079 if (!input.isTextual()) { 080 throw new IllegalStateException("found node that is not a string " + input.toString()); 081 } 082 return input.getTextValue(); 083 } 084 }); 085 REPRESENTATION_INSTANTIATORS.put(JsonNode.class, new Function<JsonNode, JsonNode>() { 086 @Override 087 public JsonNode apply(final JsonNode input) { 088 return input; 089 } 090 }); 091 } 092 093 private static <T> Function<JsonNode, ?> representationInstantiatorFor(final Class<T> representationType) { 094 Function<JsonNode, ?> transformer = REPRESENTATION_INSTANTIATORS.get(representationType); 095 if (transformer == null) { 096 transformer = new Function<JsonNode, T>() { 097 @Override 098 public T apply(final JsonNode input) { 099 try { 100 final Constructor<T> constructor = representationType.getConstructor(JsonNode.class); 101 return constructor.newInstance(input); 102 } catch (final Exception e) { 103 throw new IllegalArgumentException("Conversions from JsonNode to " + representationType + " are not supported"); 104 } 105 } 106 107 }; 108 REPRESENTATION_INSTANTIATORS.put(representationType, transformer); 109 } 110 return transformer; 111 } 112 113 public static JsonRepresentation newMap(final String... keyValuePairs) { 114 final JsonRepresentation repr = new JsonRepresentation(new ObjectNode(JsonNodeFactory.instance)); 115 String key = null; 116 for (final String keyOrValue : keyValuePairs) { 117 if (key != null) { 118 repr.mapPut(key, keyOrValue); 119 key = null; 120 } else { 121 key = keyOrValue; 122 } 123 } 124 if (key != null) { 125 throw new IllegalArgumentException("must provide an even number of keys and values"); 126 } 127 return repr; 128 } 129 130 public static JsonRepresentation newArray() { 131 return newArray(0); 132 } 133 134 public static JsonRepresentation newArray(final int initialSize) { 135 final ArrayNode arrayNode = new ArrayNode(JsonNodeFactory.instance); 136 for (int i = 0; i < initialSize; i++) { 137 arrayNode.addNull(); 138 } 139 return new JsonRepresentation(arrayNode); 140 } 141 142 protected final JsonNode jsonNode; 143 144 public JsonRepresentation(final JsonNode jsonNode) { 145 this.jsonNode = jsonNode; 146 } 147 148 public JsonNode asJsonNode() { 149 return jsonNode; 150 } 151 152 public int size() { 153 if (!isMap() && !isArray()) { 154 throw new IllegalStateException("not a map or an array"); 155 } 156 return jsonNode.size(); 157 } 158 159 /** 160 * Node is a value (nb: could be {@link #isNull() null}). 161 */ 162 public boolean isValue() { 163 return jsonNode.isValueNode(); 164 } 165 166 // /////////////////////////////////////////////////////////////////////// 167 // getRepresentation 168 // /////////////////////////////////////////////////////////////////////// 169 170 public JsonRepresentation getRepresentation(final String pathTemplate, final Object... args) { 171 final String pathStr = String.format(pathTemplate, args); 172 173 final JsonNode node = getNode(pathStr); 174 175 if (representsNull(node)) { 176 return null; 177 } 178 179 return new JsonRepresentation(node); 180 } 181 182 // /////////////////////////////////////////////////////////////////////// 183 // isArray, getArray, asArray 184 // /////////////////////////////////////////////////////////////////////// 185 186 public boolean isArray(final String path) { 187 return isArray(getNode(path)); 188 } 189 190 public boolean isArray() { 191 return isArray(asJsonNode()); 192 } 193 194 private boolean isArray(final JsonNode node) { 195 return !representsNull(node) && node.isArray(); 196 } 197 198 public JsonRepresentation getArray(final String path) { 199 return getArray(path, getNode(path)); 200 } 201 202 public JsonRepresentation asArray() { 203 return getArray(null, asJsonNode()); 204 } 205 206 private JsonRepresentation getArray(final String path, final JsonNode node) { 207 if (representsNull(node)) { 208 return null; 209 } 210 211 if (!isArray(node)) { 212 throw new IllegalArgumentException(formatExMsg(path, "is not an array")); 213 } 214 return new JsonRepresentation(node); 215 } 216 217 public JsonRepresentation getArrayEnsured(final String path) { 218 return getArrayEnsured(path, getNode(path)); 219 } 220 221 private JsonRepresentation getArrayEnsured(final String path, final JsonNode node) { 222 if (representsNull(node)) { 223 return null; 224 } 225 return new JsonRepresentation(node).ensureArray(); 226 } 227 228 // /////////////////////////////////////////////////////////////////////// 229 // isMap, getMap, asMap 230 // /////////////////////////////////////////////////////////////////////// 231 232 public boolean isMap(final String path) { 233 return isMap(getNode(path)); 234 } 235 236 public boolean isMap() { 237 return isMap(asJsonNode()); 238 } 239 240 private boolean isMap(final JsonNode node) { 241 return !representsNull(node) && !node.isArray() && !node.isValueNode(); 242 } 243 244 public JsonRepresentation getMap(final String path) { 245 return getMap(path, getNode(path)); 246 } 247 248 public JsonRepresentation asMap() { 249 return getMap(null, asJsonNode()); 250 } 251 252 private JsonRepresentation getMap(final String path, final JsonNode node) { 253 if (representsNull(node)) { 254 return null; 255 } 256 if (isArray(node) || node.isValueNode()) { 257 throw new IllegalArgumentException(formatExMsg(path, "is not a map")); 258 } 259 return new JsonRepresentation(node); 260 } 261 262 // /////////////////////////////////////////////////////////////////////// 263 // isNumber 264 // /////////////////////////////////////////////////////////////////////// 265 266 public boolean isNumber(final String path) { 267 return isNumber(getNode(path)); 268 } 269 270 public boolean isNumber() { 271 return isNumber(asJsonNode()); 272 } 273 274 private boolean isNumber(final JsonNode node) { 275 return !representsNull(node) && node.isValueNode() && node.isNumber(); 276 } 277 278 public Number asNumber() { 279 return getNumber(null, asJsonNode()); 280 } 281 282 private Number getNumber(final String path, final JsonNode node) { 283 if (representsNull(node)) { 284 return null; 285 } 286 checkValue(path, node, "a number"); 287 if (!node.isNumber()) { 288 throw new IllegalArgumentException(formatExMsg(path, "is not a number")); 289 } 290 return node.getNumberValue(); 291 } 292 293 294 // /////////////////////////////////////////////////////////////////////// 295 // isIntegralNumber, getIntegralNumber, asIntegralNumber 296 // /////////////////////////////////////////////////////////////////////// 297 298 /** 299 * Is a long, an int or a {@link BigInteger}. 300 */ 301 public boolean isIntegralNumber(final String path) { 302 return isIntegralNumber(getNode(path)); 303 } 304 305 /** 306 * Is a long, an int or a {@link BigInteger}. 307 */ 308 public boolean isIntegralNumber() { 309 return isIntegralNumber(asJsonNode()); 310 } 311 312 private boolean isIntegralNumber(final JsonNode node) { 313 return !representsNull(node) && node.isValueNode() && node.isIntegralNumber(); 314 } 315 316 317 // /////////////////////////////////////////////////////////////////////// 318 // getDate, asDate 319 // /////////////////////////////////////////////////////////////////////// 320 321 public final static DateTimeFormatter yyyyMMdd = DateTimeFormat.forPattern("yyyy-MM-dd"); 322 323 public java.util.Date getDate(final String path) { 324 return getDate(path, getNode(path)); 325 } 326 327 public java.util.Date asDate() { 328 return getDate(null, asJsonNode()); 329 } 330 331 private java.util.Date getDate(final String path, final JsonNode node) { 332 if (representsNull(node)) { 333 return null; 334 } 335 checkValue(path, node, "a date"); 336 if (!node.isTextual()) { 337 throw new IllegalArgumentException(formatExMsg(path, "is not a date")); 338 } 339 final String textValue = node.getTextValue(); 340 return new java.util.Date(yyyyMMdd.parseMillis(textValue)); 341 } 342 343 // /////////////////////////////////////////////////////////////////////// 344 // getDate, asDate 345 // /////////////////////////////////////////////////////////////////////// 346 347 public final static DateTimeFormatter yyyyMMddTHHmmssZ = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ssZ"); 348 349 public java.util.Date getDateTime(final String path) { 350 return getDateTime(path, getNode(path)); 351 } 352 353 public java.util.Date asDateTime() { 354 return getDateTime(null, asJsonNode()); 355 } 356 357 private java.util.Date getDateTime(final String path, final JsonNode node) { 358 if (representsNull(node)) { 359 return null; 360 } 361 checkValue(path, node, "a date-time"); 362 if (!node.isTextual()) { 363 throw new IllegalArgumentException(formatExMsg(path, "is not a date-time")); 364 } 365 final String textValue = node.getTextValue(); 366 return new java.util.Date(yyyyMMddTHHmmssZ.parseMillis(textValue)); 367 } 368 369 // /////////////////////////////////////////////////////////////////////// 370 // isBoolean, getBoolean, asBoolean 371 // /////////////////////////////////////////////////////////////////////// 372 373 public boolean isBoolean(final String path) { 374 return isBoolean(getNode(path)); 375 } 376 377 public boolean isBoolean() { 378 return isBoolean(asJsonNode()); 379 } 380 381 private boolean isBoolean(final JsonNode node) { 382 return !representsNull(node) && node.isValueNode() && node.isBoolean(); 383 } 384 385 /** 386 * Use {@link #isBoolean(String)} to check first, if required. 387 */ 388 public Boolean getBoolean(final String path) { 389 return getBoolean(path, getNode(path)); 390 } 391 392 /** 393 * Use {@link #isBoolean()} to check first, if required. 394 */ 395 public Boolean asBoolean() { 396 return getBoolean(null, asJsonNode()); 397 } 398 399 private Boolean getBoolean(final String path, final JsonNode node) { 400 if (representsNull(node)) { 401 return null; 402 } 403 checkValue(path, node, "a boolean"); 404 if (!node.isBoolean()) { 405 throw new IllegalArgumentException(formatExMsg(path, "is not a boolean")); 406 } 407 return node.getBooleanValue(); 408 } 409 410 // /////////////////////////////////////////////////////////////////////// 411 // getByte, asByte 412 // /////////////////////////////////////////////////////////////////////// 413 414 /** 415 * Use {@link #isIntegralNumber(String)} to test if number (it is not possible to check if a byte, however). 416 */ 417 public Byte getByte(final String path) { 418 final JsonNode node = getNode(path); 419 return getByte(path, node); 420 } 421 422 /** 423 * Use {@link #isIntegralNumber()} to test if number (it is not possible to check if a byte, however). 424 */ 425 public Byte asByte() { 426 return getByte(null, asJsonNode()); 427 } 428 429 private Byte getByte(final String path, final JsonNode node) { 430 if (representsNull(node)) { 431 return null; 432 } 433 checkValue(path, node, "an byte"); 434 if (!node.isNumber()) { 435 // there is no node.isByte() 436 throw new IllegalArgumentException(formatExMsg(path, "is not a number")); 437 } 438 return node.getNumberValue().byteValue(); 439 } 440 441 // /////////////////////////////////////////////////////////////////////// 442 // getShort, asShort 443 // /////////////////////////////////////////////////////////////////////// 444 445 /** 446 * Use {@link #isIntegralNumber(String)} to check if number (it is not possible to check if a short, however). 447 */ 448 public Short getShort(final String path) { 449 final JsonNode node = getNode(path); 450 return getShort(path, node); 451 } 452 453 /** 454 * Use {@link #isIntegralNumber()} to check if number (it is not possible to check if a short, however). 455 */ 456 public Short asShort() { 457 return getShort(null, asJsonNode()); 458 } 459 460 private Short getShort(final String path, final JsonNode node) { 461 if (representsNull(node)) { 462 return null; 463 } 464 checkValue(path, node, "an short"); 465 if (!node.isNumber()) { 466 // there is no node.isShort() 467 throw new IllegalArgumentException(formatExMsg(path, "is not a number")); 468 } 469 return node.getNumberValue().shortValue(); 470 } 471 472 473 // /////////////////////////////////////////////////////////////////////// 474 // getChar, asChar 475 // /////////////////////////////////////////////////////////////////////// 476 477 /** 478 * Use {@link #isString(String)} to check if string (it is not possible to check if a character, however). 479 */ 480 public Character getChar(final String path) { 481 final JsonNode node = getNode(path); 482 return getChar(path, node); 483 } 484 485 /** 486 * Use {@link #isString()} to check if string (it is not possible to check if a character, however). 487 */ 488 public Character asChar() { 489 return getChar(null, asJsonNode()); 490 } 491 492 private Character getChar(final String path, final JsonNode node) { 493 if (representsNull(node)) { 494 return null; 495 } 496 checkValue(path, node, "an short"); 497 if (!node.isTextual()) { 498 throw new IllegalArgumentException(formatExMsg(path, "is not textual")); 499 } 500 final String textValue = node.getTextValue(); 501 if(textValue == null || textValue.length() == 0) { 502 return null; 503 } 504 return textValue.charAt(0); 505 } 506 507 508 // /////////////////////////////////////////////////////////////////////// 509 // isInt, getInt, asInt 510 // /////////////////////////////////////////////////////////////////////// 511 512 public boolean isInt(final String path) { 513 return isInt(getNode(path)); 514 } 515 516 public boolean isInt() { 517 return isInt(asJsonNode()); 518 } 519 520 private boolean isInt(final JsonNode node) { 521 return !representsNull(node) && node.isValueNode() && node.isInt(); 522 } 523 524 /** 525 * Use {@link #isInt(String)} to check first, if required. 526 */ 527 public Integer getInt(final String path) { 528 final JsonNode node = getNode(path); 529 return getInt(path, node); 530 } 531 532 /** 533 * Use {@link #isInt()} to check first, if required. 534 */ 535 public Integer asInt() { 536 return getInt(null, asJsonNode()); 537 } 538 539 private Integer getInt(final String path, final JsonNode node) { 540 if (representsNull(node)) { 541 return null; 542 } 543 checkValue(path, node, "an int"); 544 if (!node.isInt()) { 545 throw new IllegalArgumentException(formatExMsg(path, "is not an int")); 546 } 547 return node.getIntValue(); 548 } 549 550 551 // /////////////////////////////////////////////////////////////////////// 552 // isLong, getLong, asLong 553 // /////////////////////////////////////////////////////////////////////// 554 555 public boolean isLong(final String path) { 556 return isLong(getNode(path)); 557 } 558 559 public boolean isLong() { 560 return isLong(asJsonNode()); 561 } 562 563 private boolean isLong(final JsonNode node) { 564 return !representsNull(node) && node.isValueNode() && node.isLong(); 565 } 566 567 /** 568 * Use {@link #isLong(String)} to check first, if required. 569 */ 570 public Long getLong(final String path) { 571 final JsonNode node = getNode(path); 572 return getLong(path, node); 573 } 574 575 /** 576 * Use {@link #isLong()} to check first, if required. 577 */ 578 public Long asLong() { 579 return getLong(null, asJsonNode()); 580 } 581 582 private Long getLong(final String path, final JsonNode node) { 583 if (representsNull(node)) { 584 return null; 585 } 586 checkValue(path, node, "a long"); 587 if (!node.isLong()) { 588 throw new IllegalArgumentException(formatExMsg(path, "is not a long")); 589 } 590 return node.getLongValue(); 591 } 592 593 // /////////////////////////////////////////////////////////////////////// 594 // getFloat, asFloat 595 // /////////////////////////////////////////////////////////////////////// 596 597 /** 598 * Use {@link #isNumber(String)} to test if number (it is not possible to check if a float, however). 599 */ 600 public Float getFloat(final String path) { 601 final JsonNode node = getNode(path); 602 return getFloat(path, node); 603 } 604 605 /** 606 * Use {@link #isNumber()} to test if number (it is not possible to check if a float, however). 607 */ 608 public Float asFloat() { 609 return getFloat(null, asJsonNode()); 610 } 611 612 private Float getFloat(final String path, final JsonNode node) { 613 if (representsNull(node)) { 614 return null; 615 } 616 checkValue(path, node, "a float"); 617 if (!node.isNumber()) { 618 throw new IllegalArgumentException(formatExMsg(path, "is not a number")); 619 } 620 return node.getNumberValue().floatValue(); 621 } 622 623 624 // /////////////////////////////////////////////////////////////////////// 625 // isDouble, getDouble, asDouble 626 // /////////////////////////////////////////////////////////////////////// 627 628 public boolean isDouble(final String path) { 629 return isDouble(getNode(path)); 630 } 631 632 public boolean isDouble() { 633 return isDouble(asJsonNode()); 634 } 635 636 private boolean isDouble(final JsonNode node) { 637 return !representsNull(node) && node.isValueNode() && node.isDouble(); 638 } 639 640 /** 641 * Use {@link #isDouble(String)} to check first, if required. 642 */ 643 public Double getDouble(final String path) { 644 final JsonNode node = getNode(path); 645 return getDouble(path, node); 646 } 647 648 /** 649 * Use {@link #isDouble()} to check first, if required. 650 */ 651 public Double asDouble() { 652 return getDouble(null, asJsonNode()); 653 } 654 655 private Double getDouble(final String path, final JsonNode node) { 656 if (representsNull(node)) { 657 return null; 658 } 659 checkValue(path, node, "a double"); 660 if (!node.isDouble()) { 661 throw new IllegalArgumentException(formatExMsg(path, "is not a double")); 662 } 663 return node.getDoubleValue(); 664 } 665 666 // /////////////////////////////////////////////////////////////////////// 667 // isBigInteger, getBigInteger, asBigInteger 668 // /////////////////////////////////////////////////////////////////////// 669 670 public boolean isBigInteger(final String path) { 671 return isBigInteger(getNode(path)); 672 } 673 674 public boolean isBigInteger() { 675 return isBigInteger(asJsonNode()); 676 } 677 678 private boolean isBigInteger(final JsonNode node) { 679 return !representsNull(node) && node.isValueNode() && node.isBigInteger(); 680 } 681 682 /** 683 * Use {@link #isBigInteger(String)} to check first, if required. 684 */ 685 public BigInteger getBigInteger(final String path) { 686 final JsonNode node = getNode(path); 687 return getBigInteger(path, node); 688 } 689 690 /** 691 * Use {@link #isBigInteger()} to check first, if required. 692 */ 693 public BigInteger asBigInteger() { 694 return getBigInteger(null, asJsonNode()); 695 } 696 697 private BigInteger getBigInteger(final String path, final JsonNode node) { 698 if (representsNull(node)) { 699 return null; 700 } 701 checkValue(path, node, "a biginteger"); 702 if (!node.isBigInteger()) { 703 throw new IllegalArgumentException(formatExMsg(path, "is not a biginteger")); 704 } 705 return node.getBigIntegerValue(); 706 } 707 708 // /////////////////////////////////////////////////////////////////////// 709 // isBigDecimal, getBigDecimal, asBigDecimal 710 // /////////////////////////////////////////////////////////////////////// 711 712 public boolean isBigDecimal(final String path) { 713 return isBigDecimal(getNode(path)); 714 } 715 716 public boolean isBigDecimal() { 717 return isBigDecimal(asJsonNode()); 718 } 719 720 private boolean isBigDecimal(final JsonNode node) { 721 return !representsNull(node) && node.isValueNode() && node.isBigDecimal(); 722 } 723 724 /** 725 * Use {@link #isBigDecimal(String)} to check first, if required. 726 */ 727 public BigDecimal getBigDecimal(final String path) { 728 final JsonNode node = getNode(path); 729 return getBigDecimal(path, node); 730 } 731 732 /** 733 * Use {@link #isBigDecimal()} to check first, if required. 734 */ 735 public BigDecimal asBigDecimal() { 736 return getBigDecimal(null, asJsonNode()); 737 } 738 739 private BigDecimal getBigDecimal(final String path, final JsonNode node) { 740 if (representsNull(node)) { 741 return null; 742 } 743 checkValue(path, node, "a biginteger"); 744 if (!node.isBigDecimal()) { 745 throw new IllegalArgumentException(formatExMsg(path, "is not a biginteger")); 746 } 747 return node.getDecimalValue(); 748 } 749 750 751 // /////////////////////////////////////////////////////////////////////// 752 // getString, isString, asString 753 // /////////////////////////////////////////////////////////////////////// 754 755 public boolean isString(final String path) { 756 return isString(getNode(path)); 757 } 758 759 public boolean isString() { 760 return isString(asJsonNode()); 761 } 762 763 private boolean isString(final JsonNode node) { 764 return !representsNull(node) && node.isValueNode() && node.isTextual(); 765 } 766 767 /** 768 * Use {@link #isString(String)} to check first, if required. 769 */ 770 public String getString(final String path) { 771 final JsonNode node = getNode(path); 772 return getString(path, node); 773 } 774 775 /** 776 * Use {@link #isString()} to check first, if required. 777 */ 778 public String asString() { 779 return getString(null, asJsonNode()); 780 } 781 782 private String getString(final String path, final JsonNode node) { 783 if (representsNull(node)) { 784 return null; 785 } 786 checkValue(path, node, "a string"); 787 if (!node.isTextual()) { 788 throw new IllegalArgumentException(formatExMsg(path, "is not a string")); 789 } 790 return node.getTextValue(); 791 } 792 793 public String asArg() { 794 if (isValue()) { 795 return asJsonNode().getValueAsText(); 796 } else { 797 return asJsonNode().toString(); 798 } 799 } 800 801 // /////////////////////////////////////////////////////////////////////// 802 // isLink, getLink, asLink 803 // /////////////////////////////////////////////////////////////////////// 804 805 public boolean isLink() { 806 return isLink(asJsonNode()); 807 } 808 809 public boolean isLink(final String path) { 810 return isLink(getNode(path)); 811 } 812 813 public boolean isLink(final JsonNode node) { 814 if (representsNull(node) || isArray(node) || node.isValueNode()) { 815 return false; 816 } 817 818 final LinkRepresentation link = new LinkRepresentation(node); 819 if (link.getHref() == null) { 820 return false; 821 } 822 return true; 823 } 824 825 /** 826 * Use {@link #isLink(String)} to check first, if required. 827 */ 828 public LinkRepresentation getLink(final String path) { 829 return getLink(path, getNode(path)); 830 } 831 832 /** 833 * Use {@link #isLink()} to check first, if required. 834 */ 835 public LinkRepresentation asLink() { 836 return getLink(null, asJsonNode()); 837 } 838 839 private LinkRepresentation getLink(final String path, final JsonNode node) { 840 if (representsNull(node)) { 841 return null; 842 } 843 844 if (isArray(node)) { 845 throw new IllegalArgumentException(formatExMsg(path, "is an array that does not represent a link")); 846 } 847 if (node.isValueNode()) { 848 throw new IllegalArgumentException(formatExMsg(path, "is a value that does not represent a link")); 849 } 850 851 final LinkRepresentation link = new LinkRepresentation(node); 852 if (link.getHref() == null) { 853 throw new IllegalArgumentException(formatExMsg(path, "is a map that does not fully represent a link")); 854 } 855 return link; 856 } 857 858 // /////////////////////////////////////////////////////////////////////// 859 // getNull, isNull 860 // /////////////////////////////////////////////////////////////////////// 861 862 public boolean isNull() { 863 return isNull(asJsonNode()); 864 } 865 866 /** 867 * Indicates that the wrapped node has <tt>null</tt> value (ie 868 * {@link JsonRepresentation#isNull()}), or returns <tt>null</tt> if there 869 * was no node with the provided path. 870 */ 871 public Boolean isNull(final String path) { 872 return isNull(getNode(path)); 873 } 874 875 private Boolean isNull(final JsonNode node) { 876 if (node == null || node.isMissingNode()) { 877 // not exclude if node.isNull, cos that's the point of this. 878 return null; 879 } 880 return node.isNull(); 881 } 882 883 /** 884 * Either returns a {@link JsonRepresentation} that indicates that the 885 * wrapped node has <tt>null</tt> value (ie 886 * {@link JsonRepresentation#isNull()}), or returns <tt>null</tt> if there 887 * was no node with the provided path. 888 * 889 * <p> 890 * Use {@link #isNull(String)} to check first, if required. 891 */ 892 public JsonRepresentation getNull(final String path) { 893 return getNull(path, getNode(path)); 894 } 895 896 /** 897 * Either returns a {@link JsonRepresentation} that indicates that the 898 * wrapped node has <tt>null</tt> value (ie 899 * {@link JsonRepresentation#isNull()}), or returns <tt>null</tt> if there 900 * was no node with the provided path. 901 * 902 * <p> 903 * Use {@link #isNull()} to check first, if required. 904 */ 905 public JsonRepresentation asNull() { 906 return getNull(null, asJsonNode()); 907 } 908 909 private JsonRepresentation getNull(final String path, final JsonNode node) { 910 if (node == null || node.isMissingNode()) { 911 // exclude if node.isNull, cos that's the point of this. 912 return null; 913 } 914 checkValue(path, node, "the null value"); 915 if (!node.isNull()) { 916 throw new IllegalArgumentException(formatExMsg(path, "is not the null value")); 917 } 918 return new JsonRepresentation(node); 919 } 920 921 // /////////////////////////////////////////////////////////////////////// 922 // mapValueAsLink 923 // /////////////////////////////////////////////////////////////////////// 924 925 /** 926 * Convert a representation that contains a single node representing a link 927 * into a {@link LinkRepresentation}. 928 */ 929 public LinkRepresentation mapValueAsLink() { 930 if (asJsonNode().size() != 1) { 931 throw new IllegalStateException("does not represent link"); 932 } 933 final String linkPropertyName = asJsonNode().getFieldNames().next(); 934 return getLink(linkPropertyName); 935 } 936 937 // /////////////////////////////////////////////////////////////////////// 938 // asInputStream 939 // /////////////////////////////////////////////////////////////////////// 940 941 public InputStream asInputStream() { 942 return JsonNodeUtils.asInputStream(jsonNode); 943 } 944 945 // /////////////////////////////////////////////////////////////////////// 946 // asArrayNode, asObjectNode 947 // /////////////////////////////////////////////////////////////////////// 948 949 /** 950 * Convert underlying representation into an array. 951 */ 952 protected ArrayNode asArrayNode() { 953 if (!isArray()) { 954 throw new IllegalStateException("does not represent array"); 955 } 956 return (ArrayNode) asJsonNode(); 957 } 958 959 /** 960 * Convert underlying representation into an object (map). 961 */ 962 protected ObjectNode asObjectNode() { 963 if (!isMap()) { 964 throw new IllegalStateException("does not represent map"); 965 } 966 return (ObjectNode) asJsonNode(); 967 } 968 969 // /////////////////////////////////////////////////////////////////////// 970 // asT 971 // /////////////////////////////////////////////////////////////////////// 972 973 /** 974 * Convenience to simply "downcast". 975 * 976 * <p> 977 * In fact, the method creates a new instance of the specified type, which 978 * shares the underlying {@link #jsonNode jsonNode}. 979 */ 980 public <T extends JsonRepresentation> T as(final Class<T> cls) { 981 try { 982 final Constructor<T> constructor = cls.getConstructor(JsonNode.class); 983 return constructor.newInstance(jsonNode); 984 } catch (final Exception e) { 985 throw new RuntimeException(e); 986 } 987 } 988 989 // /////////////////////////////////////////////////////////////////////// 990 // asUrlEncoded 991 // /////////////////////////////////////////////////////////////////////// 992 993 public String asUrlEncoded() { 994 return UrlEncodingUtils.urlEncode(asJsonNode()); 995 } 996 997 // /////////////////////////////////////////////////////////////////////// 998 // mutable (array) 999 // /////////////////////////////////////////////////////////////////////// 1000 1001 public void arrayAdd(final Object value) { 1002 if (!isArray()) { 1003 throw new IllegalStateException("does not represent array"); 1004 } 1005 asArrayNode().add(new POJONode(value)); 1006 } 1007 1008 public void arrayAdd(final JsonRepresentation value) { 1009 if (!isArray()) { 1010 throw new IllegalStateException("does not represent array"); 1011 } 1012 asArrayNode().add(value.asJsonNode()); 1013 } 1014 1015 public void arrayAdd(final String value) { 1016 if (!isArray()) { 1017 throw new IllegalStateException("does not represent array"); 1018 } 1019 asArrayNode().add(value); 1020 } 1021 1022 public void arrayAdd(final JsonNode value) { 1023 if (!isArray()) { 1024 throw new IllegalStateException("does not represent array"); 1025 } 1026 asArrayNode().add(value); 1027 } 1028 1029 public void arrayAdd(final long value) { 1030 if (!isArray()) { 1031 throw new IllegalStateException("does not represent array"); 1032 } 1033 asArrayNode().add(value); 1034 } 1035 1036 public void arrayAdd(final int value) { 1037 if (!isArray()) { 1038 throw new IllegalStateException("does not represent array"); 1039 } 1040 asArrayNode().add(value); 1041 } 1042 1043 public void arrayAdd(final double value) { 1044 if (!isArray()) { 1045 throw new IllegalStateException("does not represent array"); 1046 } 1047 asArrayNode().add(value); 1048 } 1049 1050 public void arrayAdd(final float value) { 1051 if (!isArray()) { 1052 throw new IllegalStateException("does not represent array"); 1053 } 1054 asArrayNode().add(value); 1055 } 1056 1057 public void arrayAdd(final boolean value) { 1058 if (!isArray()) { 1059 throw new IllegalStateException("does not represent array"); 1060 } 1061 asArrayNode().add(value); 1062 } 1063 1064 public Iterable<JsonRepresentation> arrayIterable() { 1065 return arrayIterable(JsonRepresentation.class); 1066 } 1067 1068 public <T> Iterable<T> arrayIterable(final Class<T> requiredType) { 1069 return new Iterable<T>() { 1070 @Override 1071 public Iterator<T> iterator() { 1072 return arrayIterator(requiredType); 1073 } 1074 }; 1075 } 1076 1077 public Iterator<JsonRepresentation> arrayIterator() { 1078 return arrayIterator(JsonRepresentation.class); 1079 } 1080 1081 public <T> Iterator<T> arrayIterator(final Class<T> requiredType) { 1082 ensureIsAnArrayAtLeastAsLargeAs(0); 1083 final Function<JsonNode, ?> transformer = representationInstantiatorFor(requiredType); 1084 final ArrayNode arrayNode = (ArrayNode) jsonNode; 1085 final Iterator<JsonNode> iterator = arrayNode.iterator(); 1086 // necessary to do in two steps 1087 final Function<JsonNode, T> typedTransformer = asT(transformer); 1088 return Iterators.transform(iterator, typedTransformer); 1089 } 1090 1091 @SuppressWarnings("unchecked") 1092 private static <T> Function<JsonNode, T> asT(final Function<JsonNode, ?> transformer) { 1093 return (Function<JsonNode, T>) transformer; 1094 } 1095 1096 public JsonRepresentation arrayGet(final int i) { 1097 ensureIsAnArrayAtLeastAsLargeAs(i+1); 1098 return new JsonRepresentation(jsonNode.get(i)); 1099 } 1100 1101 public void arraySetElementAt(final int i, final JsonRepresentation objectRepr) { 1102 ensureIsAnArrayAtLeastAsLargeAs(i+1); 1103 if (objectRepr.isArray()) { 1104 throw new IllegalArgumentException("Representation being set cannot be an array"); 1105 } 1106 // can safely downcast because *this* representation is an array 1107 final ArrayNode arrayNode = (ArrayNode) jsonNode; 1108 arrayNode.set(i, objectRepr.asJsonNode()); 1109 } 1110 1111 private void ensureIsAnArrayAtLeastAsLargeAs(final int i) { 1112 if (!jsonNode.isArray()) { 1113 throw new IllegalStateException("Is not an array"); 1114 } 1115 if (i > size()) { 1116 throw new IndexOutOfBoundsException("array has only " + size() + " elements"); 1117 } 1118 } 1119 1120 // /////////////////////////////////////////////////////////////////////// 1121 // mutable (map) 1122 // /////////////////////////////////////////////////////////////////////// 1123 1124 public boolean mapHas(final String key) { 1125 if (!isMap()) { 1126 throw new IllegalStateException("does not represent map"); 1127 } 1128 ObjectNode node = asObjectNode(); 1129 1130 final String[] paths = key.split("\\."); 1131 for (int i = 0; i < paths.length; i++) { 1132 final String path = paths[i]; 1133 final boolean has = node.has(path); 1134 if (!has) { 1135 return false; 1136 } 1137 if (i + 1 < paths.length) { 1138 // not on last 1139 final JsonNode subNode = node.get(path); 1140 if (!subNode.isObject()) { 1141 return false; 1142 } 1143 node = (ObjectNode) subNode; 1144 } 1145 } 1146 return true; 1147 } 1148 1149 public void mapPut(final String key, final List<Object> value) { 1150 if (!isMap()) { 1151 throw new IllegalStateException("does not represent map"); 1152 } 1153 if (value == null) { 1154 return; 1155 } 1156 final JsonRepresentation array = JsonRepresentation.newArray(); 1157 for (final Object v : value) { 1158 array.arrayAdd(v); 1159 } 1160 mapPut(key, array); 1161 } 1162 1163 public void mapPut(final String key, final Object value) { 1164 if (!isMap()) { 1165 throw new IllegalStateException("does not represent map"); 1166 } 1167 final Path path = Path.parse(key); 1168 final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead()); 1169 node.put(path.getTail(), value != null? new POJONode(value): NullNode.getInstance() ); 1170 } 1171 1172 public void mapPut(final String key, final JsonRepresentation value) { 1173 if (!isMap()) { 1174 throw new IllegalStateException("does not represent map"); 1175 } 1176 if (value == null) { 1177 return; 1178 } 1179 final Path path = Path.parse(key); 1180 final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead()); 1181 node.put(path.getTail(), value.asJsonNode()); 1182 } 1183 1184 public void mapPut(final String key, final String value) { 1185 if (!isMap()) { 1186 throw new IllegalStateException("does not represent map"); 1187 } 1188 if (value == null) { 1189 return; 1190 } 1191 final Path path = Path.parse(key); 1192 final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead()); 1193 node.put(path.getTail(), value); 1194 } 1195 1196 public void mapPut(final String key, final JsonNode value) { 1197 if (!isMap()) { 1198 throw new IllegalStateException("does not represent map"); 1199 } 1200 if (value == null) { 1201 return; 1202 } 1203 final Path path = Path.parse(key); 1204 final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead()); 1205 node.put(path.getTail(), value); 1206 } 1207 1208 public void mapPut(final String key, final long value) { 1209 if (!isMap()) { 1210 throw new IllegalStateException("does not represent map"); 1211 } 1212 final Path path = Path.parse(key); 1213 final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead()); 1214 node.put(path.getTail(), value); 1215 } 1216 1217 public void mapPut(final String key, final int value) { 1218 if (!isMap()) { 1219 throw new IllegalStateException("does not represent map"); 1220 } 1221 final Path path = Path.parse(key); 1222 final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead()); 1223 node.put(path.getTail(), value); 1224 } 1225 1226 public void mapPut(final String key, final double value) { 1227 if (!isMap()) { 1228 throw new IllegalStateException("does not represent map"); 1229 } 1230 final Path path = Path.parse(key); 1231 final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead()); 1232 node.put(path.getTail(), value); 1233 } 1234 1235 public void mapPut(final String key, final float value) { 1236 if (!isMap()) { 1237 throw new IllegalStateException("does not represent map"); 1238 } 1239 final Path path = Path.parse(key); 1240 final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead()); 1241 node.put(path.getTail(), value); 1242 } 1243 1244 public void mapPut(final String key, final boolean value) { 1245 if (!isMap()) { 1246 throw new IllegalStateException("does not represent map"); 1247 } 1248 final Path path = Path.parse(key); 1249 final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead()); 1250 node.put(path.getTail(), value); 1251 } 1252 1253 private static class Path { 1254 private final List<String> head; 1255 private final String tail; 1256 1257 private Path(final List<String> head, final String tail) { 1258 this.head = Collections.unmodifiableList(head); 1259 this.tail = tail; 1260 } 1261 1262 public List<String> getHead() { 1263 return head; 1264 } 1265 1266 public String getTail() { 1267 return tail; 1268 } 1269 1270 public static Path parse(final String pathStr) { 1271 final List<String> keyList = Lists.newArrayList(Arrays.asList(pathStr.split("\\."))); 1272 if (keyList.size() == 0) { 1273 throw new IllegalArgumentException(String.format("Malformed path '%s'", pathStr)); 1274 } 1275 final String tail = keyList.remove(keyList.size() - 1); 1276 return new Path(keyList, tail); 1277 } 1278 } 1279 1280 public Iterable<Map.Entry<String, JsonRepresentation>> mapIterable() { 1281 ensureIsAMap(); 1282 return new Iterable<Map.Entry<String, JsonRepresentation>>() { 1283 1284 @Override 1285 public Iterator<Entry<String, JsonRepresentation>> iterator() { 1286 return mapIterator(); 1287 } 1288 }; 1289 } 1290 1291 public Iterator<Map.Entry<String, JsonRepresentation>> mapIterator() { 1292 ensureIsAMap(); 1293 return Iterators.transform(jsonNode.getFields(), MAP_ENTRY_JSON_NODE_TO_JSON_REPRESENTATION); 1294 } 1295 1296 private void ensureIsAMap() { 1297 if (!jsonNode.isObject()) { 1298 throw new IllegalStateException("Is not a map"); 1299 } 1300 } 1301 1302 private final static Function<Entry<String, JsonNode>, Entry<String, JsonRepresentation>> MAP_ENTRY_JSON_NODE_TO_JSON_REPRESENTATION = new Function<Entry<String, JsonNode>, Entry<String, JsonRepresentation>>() { 1303 1304 @Override 1305 public Entry<String, JsonRepresentation> apply(final Entry<String, JsonNode> input) { 1306 return new Map.Entry<String, JsonRepresentation>() { 1307 1308 @Override 1309 public String getKey() { 1310 return input.getKey(); 1311 } 1312 1313 @Override 1314 public JsonRepresentation getValue() { 1315 return new JsonRepresentation(input.getValue()); 1316 } 1317 1318 @Override 1319 public JsonRepresentation setValue(final JsonRepresentation value) { 1320 final JsonNode setValue = input.setValue(value.asJsonNode()); 1321 return new JsonRepresentation(setValue); 1322 } 1323 }; 1324 } 1325 }; 1326 1327 // /////////////////////////////////////////////////////////////////////// 1328 // helpers 1329 // /////////////////////////////////////////////////////////////////////// 1330 1331 /** 1332 * A reciprocal of the behaviour of the automatic dereferencing of arrays 1333 * that occurs when there is only a single instance. 1334 * 1335 * @see #toJsonNode(List) 1336 */ 1337 public JsonRepresentation ensureArray() { 1338 if (jsonNode.isArray()) { 1339 return this; 1340 } 1341 final JsonRepresentation arrayRepr = JsonRepresentation.newArray(); 1342 arrayRepr.arrayAdd(jsonNode); 1343 return arrayRepr; 1344 } 1345 1346 private JsonNode getNode(final String path) { 1347 JsonNode jsonNode = this.jsonNode; 1348 final List<String> keys = PathNode.split(path); 1349 for (final String key : keys) { 1350 final PathNode pathNode = PathNode.parse(key); 1351 if (!pathNode.getKey().isEmpty()) { 1352 jsonNode = jsonNode.path(pathNode.getKey()); 1353 } else { 1354 // pathNode is criteria only; don't change jsonNode 1355 } 1356 if (jsonNode.isNull()) { 1357 return jsonNode; 1358 } 1359 if (!pathNode.hasCriteria()) { 1360 continue; 1361 } 1362 if (!jsonNode.isArray()) { 1363 return NullNode.getInstance(); 1364 } 1365 jsonNode = matching(jsonNode, pathNode); 1366 if (jsonNode.isNull()) { 1367 return jsonNode; 1368 } 1369 } 1370 return jsonNode; 1371 } 1372 1373 private JsonNode matching(final JsonNode jsonNode, final PathNode pathNode) { 1374 final JsonRepresentation asList = new JsonRepresentation(jsonNode); 1375 final Iterable<JsonNode> filtered = Iterables.filter(asList.arrayIterable(JsonNode.class), new Predicate<JsonNode>() { 1376 @Override 1377 public boolean apply(final JsonNode input) { 1378 return pathNode.matches(new JsonRepresentation(input)); 1379 } 1380 }); 1381 final List<JsonNode> matching = Lists.newArrayList(filtered); 1382 return toJsonNode(matching); 1383 } 1384 1385 private static JsonNode toJsonNode(final List<JsonNode> matching) { 1386 switch (matching.size()) { 1387 case 0: 1388 return NullNode.getInstance(); 1389 case 1: 1390 return matching.get(0); 1391 default: 1392 final ArrayNode arrayNode = new ArrayNode(JsonNodeFactory.instance); 1393 arrayNode.addAll(matching); 1394 return arrayNode; 1395 } 1396 } 1397 1398 private static void checkValue(final String path, final JsonNode node, final String requiredType) { 1399 if (node.isValueNode()) { 1400 return; 1401 } 1402 throw new IllegalArgumentException(formatExMsg(path, "is not " + requiredType)); 1403 } 1404 1405 private static boolean representsNull(final JsonNode node) { 1406 return node == null || node.isMissingNode() || node.isNull(); 1407 } 1408 1409 private static String formatExMsg(final String pathIfAny, final String errorText) { 1410 final StringBuilder buf = new StringBuilder(); 1411 if (pathIfAny != null) { 1412 buf.append("'").append(pathIfAny).append("' "); 1413 } 1414 buf.append(errorText); 1415 return buf.toString(); 1416 } 1417 1418 1419 // /////////////////////////////////////////////////////////////////////// 1420 // equals and hashcode 1421 // /////////////////////////////////////////////////////////////////////// 1422 1423 @Override 1424 public int hashCode() { 1425 final int prime = 31; 1426 int result = 1; 1427 result = prime * result + ((jsonNode == null) ? 0 : jsonNode.hashCode()); 1428 return result; 1429 } 1430 1431 @Override 1432 public boolean equals(Object obj) { 1433 if (this == obj) 1434 return true; 1435 if (obj == null) 1436 return false; 1437 if (getClass() != obj.getClass()) 1438 return false; 1439 JsonRepresentation other = (JsonRepresentation) obj; 1440 if (jsonNode == null) { 1441 if (other.jsonNode != null) 1442 return false; 1443 } else if (!jsonNode.equals(other.jsonNode)) 1444 return false; 1445 return true; 1446 } 1447 1448 // /////////////////////////////////////////////////////////////////////// 1449 // toString 1450 // /////////////////////////////////////////////////////////////////////// 1451 1452 @Override 1453 public String toString() { 1454 return jsonNode.toString(); 1455 } 1456 1457 1458 1459}