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 */
019 package org.apache.isis.viewer.restfulobjects.applib;
020
021 import java.io.InputStream;
022 import java.lang.reflect.Constructor;
023 import java.math.BigDecimal;
024 import java.math.BigInteger;
025 import java.util.Arrays;
026 import java.util.Collections;
027 import java.util.Iterator;
028 import java.util.List;
029 import java.util.Map;
030 import java.util.Map.Entry;
031
032 import com.google.common.base.Function;
033 import com.google.common.base.Predicate;
034 import com.google.common.collect.Iterables;
035 import com.google.common.collect.Iterators;
036 import com.google.common.collect.Lists;
037 import com.google.common.collect.Maps;
038
039 import org.codehaus.jackson.JsonNode;
040 import org.codehaus.jackson.node.ArrayNode;
041 import org.codehaus.jackson.node.JsonNodeFactory;
042 import org.codehaus.jackson.node.NullNode;
043 import org.codehaus.jackson.node.ObjectNode;
044 import org.codehaus.jackson.node.POJONode;
045
046 import org.apache.isis.viewer.restfulobjects.applib.links.LinkRepresentation;
047 import org.apache.isis.viewer.restfulobjects.applib.util.JsonNodeUtils;
048 import org.apache.isis.viewer.restfulobjects.applib.util.PathNode;
049 import org.apache.isis.viewer.restfulobjects.applib.util.UrlEncodingUtils;
050
051 /**
052 * A wrapper around {@link JsonNode} that provides some additional helper
053 * methods.
054 */
055 public class JsonRepresentation {
056
057 public interface LinksToSelf {
058 public LinkRepresentation getSelf();
059 }
060
061 public interface HasLinks {
062 public JsonRepresentation getLinks();
063 }
064
065 public interface HasExtensions {
066 public JsonRepresentation getExtensions();
067 }
068
069 private static Map<Class<?>, Function<JsonNode, ?>> REPRESENTATION_INSTANTIATORS = Maps.newHashMap();
070 static {
071 REPRESENTATION_INSTANTIATORS.put(String.class, new Function<JsonNode, String>() {
072 @Override
073 public String apply(final JsonNode input) {
074 if (!input.isTextual()) {
075 throw new IllegalStateException("found node that is not a string " + input.toString());
076 }
077 return input.getTextValue();
078 }
079 });
080 REPRESENTATION_INSTANTIATORS.put(JsonNode.class, new Function<JsonNode, JsonNode>() {
081 @Override
082 public JsonNode apply(final JsonNode input) {
083 return input;
084 }
085 });
086 }
087
088 private static <T> Function<JsonNode, ?> representationInstantiatorFor(final Class<T> representationType) {
089 Function<JsonNode, ?> transformer = REPRESENTATION_INSTANTIATORS.get(representationType);
090 if (transformer == null) {
091 transformer = new Function<JsonNode, T>() {
092 @Override
093 public T apply(final JsonNode input) {
094 try {
095 final Constructor<T> constructor = representationType.getConstructor(JsonNode.class);
096 return constructor.newInstance(input);
097 } catch (final Exception e) {
098 throw new IllegalArgumentException("Conversions from JsonNode to " + representationType + " are not supported");
099 }
100 }
101
102 };
103 REPRESENTATION_INSTANTIATORS.put(representationType, transformer);
104 }
105 return transformer;
106 }
107
108 public static JsonRepresentation newMap(final String... keyValuePairs) {
109 final JsonRepresentation repr = new JsonRepresentation(new ObjectNode(JsonNodeFactory.instance));
110 String key = null;
111 for (final String keyOrValue : keyValuePairs) {
112 if (key != null) {
113 repr.mapPut(key, keyOrValue);
114 key = null;
115 } else {
116 key = keyOrValue;
117 }
118 }
119 if (key != null) {
120 throw new IllegalArgumentException("must provide an even number of keys and values");
121 }
122 return repr;
123 }
124
125 public static JsonRepresentation newArray() {
126 return newArray(0);
127 }
128
129 public static JsonRepresentation newArray(final int initialSize) {
130 final ArrayNode arrayNode = new ArrayNode(JsonNodeFactory.instance);
131 for (int i = 0; i < initialSize; i++) {
132 arrayNode.addNull();
133 }
134 return new JsonRepresentation(arrayNode);
135 }
136
137 protected final JsonNode jsonNode;
138
139 public JsonRepresentation(final JsonNode jsonNode) {
140 this.jsonNode = jsonNode;
141 }
142
143 public JsonNode asJsonNode() {
144 return jsonNode;
145 }
146
147 public int size() {
148 if (!isMap() && !isArray()) {
149 throw new IllegalStateException("not a map or an array");
150 }
151 return jsonNode.size();
152 }
153
154 /**
155 * Node is a value (nb: could be {@link #isNull() null}).
156 */
157 public boolean isValue() {
158 return jsonNode.isValueNode();
159 }
160
161 // ///////////////////////////////////////////////////////////////////////
162 // getRepresentation
163 // ///////////////////////////////////////////////////////////////////////
164
165 public JsonRepresentation getRepresentation(final String pathTemplate, final Object... args) {
166 final String pathStr = String.format(pathTemplate, args);
167
168 final JsonNode node = getNode(pathStr);
169
170 if (representsNull(node)) {
171 return null;
172 }
173
174 return new JsonRepresentation(node);
175 }
176
177 // ///////////////////////////////////////////////////////////////////////
178 // isArray, getArray, asArray
179 // ///////////////////////////////////////////////////////////////////////
180
181 public boolean isArray(final String path) {
182 return isArray(getNode(path));
183 }
184
185 public boolean isArray() {
186 return isArray(asJsonNode());
187 }
188
189 private boolean isArray(final JsonNode node) {
190 return !representsNull(node) && node.isArray();
191 }
192
193 public JsonRepresentation getArray(final String path) {
194 return getArray(path, getNode(path));
195 }
196
197 public JsonRepresentation asArray() {
198 return getArray(null, asJsonNode());
199 }
200
201 private JsonRepresentation getArray(final String path, final JsonNode node) {
202 if (representsNull(node)) {
203 return null;
204 }
205
206 if (!isArray(node)) {
207 throw new IllegalArgumentException(formatExMsg(path, "is not an array"));
208 }
209 return new JsonRepresentation(node);
210 }
211
212 public JsonRepresentation getArrayEnsured(final String path) {
213 return getArrayEnsured(path, getNode(path));
214 }
215
216 private JsonRepresentation getArrayEnsured(final String path, final JsonNode node) {
217 if (representsNull(node)) {
218 return null;
219 }
220 return new JsonRepresentation(node).ensureArray();
221 }
222
223 // ///////////////////////////////////////////////////////////////////////
224 // isMap, getMap, asMap
225 // ///////////////////////////////////////////////////////////////////////
226
227 public boolean isMap(final String path) {
228 return isMap(getNode(path));
229 }
230
231 public boolean isMap() {
232 return isMap(asJsonNode());
233 }
234
235 private boolean isMap(final JsonNode node) {
236 return !representsNull(node) && !node.isArray() && !node.isValueNode();
237 }
238
239 public JsonRepresentation getMap(final String path) {
240 return getMap(path, getNode(path));
241 }
242
243 public JsonRepresentation asMap() {
244 return getMap(null, asJsonNode());
245 }
246
247 private JsonRepresentation getMap(final String path, final JsonNode node) {
248 if (representsNull(node)) {
249 return null;
250 }
251 if (isArray(node) || node.isValueNode()) {
252 throw new IllegalArgumentException(formatExMsg(path, "is not a map"));
253 }
254 return new JsonRepresentation(node);
255 }
256
257 // ///////////////////////////////////////////////////////////////////////
258 // isBoolean, getBoolean, asBoolean
259 // ///////////////////////////////////////////////////////////////////////
260
261 public boolean isBoolean(final String path) {
262 return isBoolean(getNode(path));
263 }
264
265 public boolean isBoolean() {
266 return isBoolean(asJsonNode());
267 }
268
269 private boolean isBoolean(final JsonNode node) {
270 return !representsNull(node) && node.isValueNode() && node.isBoolean();
271 }
272
273 public Boolean getBoolean(final String path) {
274 return getBoolean(path, getNode(path));
275 }
276
277 public Boolean asBoolean() {
278 return getBoolean(null, asJsonNode());
279 }
280
281 private Boolean getBoolean(final String path, final JsonNode node) {
282 if (representsNull(node)) {
283 return null;
284 }
285 checkValue(path, node, "a boolean");
286 if (!node.isBoolean()) {
287 throw new IllegalArgumentException(formatExMsg(path, "is not a boolean"));
288 }
289 return node.getBooleanValue();
290 }
291
292 // ///////////////////////////////////////////////////////////////////////
293 // isBigInteger, getBigInteger, asBigInteger
294 // ///////////////////////////////////////////////////////////////////////
295
296 public boolean isBigInteger(final String path) {
297 return isBigInteger(getNode(path));
298 }
299
300 public boolean isBigInteger() {
301 return isBigInteger(asJsonNode());
302 }
303
304 private boolean isBigInteger(final JsonNode node) {
305 return !representsNull(node) && node.isValueNode() && node.isBigInteger();
306 }
307
308 public BigInteger getBigInteger(final String path) {
309 final JsonNode node = getNode(path);
310 return getBigInteger(path, node);
311 }
312
313 public BigInteger asBigInteger() {
314 return getBigInteger(null, asJsonNode());
315 }
316
317 private BigInteger getBigInteger(final String path, final JsonNode node) {
318 if (representsNull(node)) {
319 return null;
320 }
321 checkValue(path, node, "a biginteger");
322 if (!node.isBigInteger()) {
323 throw new IllegalArgumentException(formatExMsg(path, "is not a biginteger"));
324 }
325 return node.getBigIntegerValue();
326 }
327
328 // ///////////////////////////////////////////////////////////////////////
329 // isBigDecimal, getBigDecimal, asBigDecimal
330 // ///////////////////////////////////////////////////////////////////////
331
332 public boolean isBigDecimal(final String path) {
333 return isBigDecimal(getNode(path));
334 }
335
336 public boolean isBigDecimal() {
337 return isBigDecimal(asJsonNode());
338 }
339
340 private boolean isBigDecimal(final JsonNode node) {
341 return !representsNull(node) && node.isValueNode() && node.isBigDecimal();
342 }
343
344 public BigDecimal getBigDecimal(final String path) {
345 final JsonNode node = getNode(path);
346 return getBigDecimal(path, node);
347 }
348
349 public BigDecimal asBigDecimal() {
350 return getBigDecimal(null, asJsonNode());
351 }
352
353 private BigDecimal getBigDecimal(final String path, final JsonNode node) {
354 if (representsNull(node)) {
355 return null;
356 }
357 checkValue(path, node, "a biginteger");
358 if (!node.isBigDecimal()) {
359 throw new IllegalArgumentException(formatExMsg(path, "is not a biginteger"));
360 }
361 return node.getDecimalValue();
362 }
363
364 // ///////////////////////////////////////////////////////////////////////
365 // isInt, getInt, asInt
366 // ///////////////////////////////////////////////////////////////////////
367
368 public boolean isInt(final String path) {
369 return isInt(getNode(path));
370 }
371
372 public boolean isInt() {
373 return isInt(asJsonNode());
374 }
375
376 private boolean isInt(final JsonNode node) {
377 return !representsNull(node) && node.isValueNode() && node.isInt();
378 }
379
380 public Integer getInt(final String path) {
381 final JsonNode node = getNode(path);
382 return getInt(path, node);
383 }
384
385 public Integer asInt() {
386 return getInt(null, asJsonNode());
387 }
388
389 private Integer getInt(final String path, final JsonNode node) {
390 if (representsNull(node)) {
391 return null;
392 }
393 checkValue(path, node, "an int");
394 if (!node.isInt()) {
395 throw new IllegalArgumentException(formatExMsg(path, "is not an int"));
396 }
397 return node.getIntValue();
398 }
399
400 // ///////////////////////////////////////////////////////////////////////
401 // isLong, getLong, asLong
402 // ///////////////////////////////////////////////////////////////////////
403
404 public boolean isLong(final String path) {
405 return isLong(getNode(path));
406 }
407
408 public boolean isLong() {
409 return isLong(asJsonNode());
410 }
411
412 private boolean isLong(final JsonNode node) {
413 return !representsNull(node) && node.isValueNode() && node.isLong();
414 }
415
416 public Long getLong(final String path) {
417 final JsonNode node = getNode(path);
418 return getLong(path, node);
419 }
420
421 public Long asLong() {
422 return getLong(null, asJsonNode());
423 }
424
425 private Long getLong(final String path, final JsonNode node) {
426 if (representsNull(node)) {
427 return null;
428 }
429 checkValue(path, node, "a long");
430 if (!node.isLong()) {
431 throw new IllegalArgumentException(formatExMsg(path, "is not a long"));
432 }
433 return node.getLongValue();
434 }
435
436 // ///////////////////////////////////////////////////////////////////////
437 // isDouble, getDouble, asDouble
438 // ///////////////////////////////////////////////////////////////////////
439
440 public boolean isDouble(final String path) {
441 return isDouble(getNode(path));
442 }
443
444 public boolean isDouble() {
445 return isDouble(asJsonNode());
446 }
447
448 private boolean isDouble(final JsonNode node) {
449 return !representsNull(node) && node.isValueNode() && node.isDouble();
450 }
451
452 public Double getDouble(final String path) {
453 final JsonNode node = getNode(path);
454 return getDouble(path, node);
455 }
456
457 public Double asDouble() {
458 return getDouble(null, asJsonNode());
459 }
460
461 private Double getDouble(final String path, final JsonNode node) {
462 if (representsNull(node)) {
463 return null;
464 }
465 checkValue(path, node, "a double");
466 if (!node.isDouble()) {
467 throw new IllegalArgumentException(formatExMsg(path, "is not a double"));
468 }
469 return node.getDoubleValue();
470 }
471
472 // ///////////////////////////////////////////////////////////////////////
473 // getString, isString, asString
474 // ///////////////////////////////////////////////////////////////////////
475
476 public boolean isString(final String path) {
477 return isString(getNode(path));
478 }
479
480 public boolean isString() {
481 return isString(asJsonNode());
482 }
483
484 private boolean isString(final JsonNode node) {
485 return !representsNull(node) && node.isValueNode() && node.isTextual();
486 }
487
488 public String getString(final String path) {
489 final JsonNode node = getNode(path);
490 return getString(path, node);
491 }
492
493 public String asString() {
494 return getString(null, asJsonNode());
495 }
496
497 private String getString(final String path, final JsonNode node) {
498 if (representsNull(node)) {
499 return null;
500 }
501 checkValue(path, node, "a string");
502 if (!node.isTextual()) {
503 throw new IllegalArgumentException(formatExMsg(path, "is not a string"));
504 }
505 return node.getTextValue();
506 }
507
508 public String asArg() {
509 if (isValue()) {
510 return asJsonNode().getValueAsText();
511 } else {
512 return asJsonNode().toString();
513 }
514 }
515
516 // ///////////////////////////////////////////////////////////////////////
517 // isLink, getLink, asLink
518 // ///////////////////////////////////////////////////////////////////////
519
520 public boolean isLink() {
521 return isLink(asJsonNode());
522 }
523
524 public boolean isLink(final String path) {
525 return isLink(getNode(path));
526 }
527
528 public boolean isLink(final JsonNode node) {
529 if (representsNull(node) || isArray(node) || node.isValueNode()) {
530 return false;
531 }
532
533 final LinkRepresentation link = new LinkRepresentation(node);
534 if (link.getHref() == null) {
535 return false;
536 }
537 return true;
538 }
539
540 public LinkRepresentation getLink(final String path) {
541 return getLink(path, getNode(path));
542 }
543
544 public LinkRepresentation asLink() {
545 return getLink(null, asJsonNode());
546 }
547
548 private LinkRepresentation getLink(final String path, final JsonNode node) {
549 if (representsNull(node)) {
550 return null;
551 }
552
553 if (isArray(node)) {
554 throw new IllegalArgumentException(formatExMsg(path, "is an array that does not represent a link"));
555 }
556 if (node.isValueNode()) {
557 throw new IllegalArgumentException(formatExMsg(path, "is a value that does not represent a link"));
558 }
559
560 final LinkRepresentation link = new LinkRepresentation(node);
561 if (link.getHref() == null) {
562 throw new IllegalArgumentException(formatExMsg(path, "is a map that does not fully represent a link"));
563 }
564 return link;
565 }
566
567 // ///////////////////////////////////////////////////////////////////////
568 // getNull, isNull
569 // ///////////////////////////////////////////////////////////////////////
570
571 public boolean isNull() {
572 return isNull(asJsonNode());
573 }
574
575 /**
576 * Indicates that the wrapped node has <tt>null</tt> value (ie
577 * {@link JsonRepresentation#isNull()}), or returns <tt>null</tt> if there
578 * was no node with the provided path.
579 */
580 public Boolean isNull(final String path) {
581 return isNull(getNode(path));
582 }
583
584 private Boolean isNull(final JsonNode node) {
585 if (node == null || node.isMissingNode()) {
586 // not exclude if node.isNull, cos that's the point of this.
587 return null;
588 }
589 return node.isNull();
590 }
591
592 /**
593 * Either returns a {@link JsonRepresentation} that indicates that the
594 * wrapped node has <tt>null</tt> value (ie
595 * {@link JsonRepresentation#isNull()}), or returns <tt>null</tt> if there
596 * was no node with the provided path.
597 */
598 public JsonRepresentation getNull(final String path) {
599 return getNull(path, getNode(path));
600 }
601
602 public JsonRepresentation asNull() {
603 return getNull(null, asJsonNode());
604 }
605
606 private JsonRepresentation getNull(final String path, final JsonNode node) {
607 if (node == null || node.isMissingNode()) {
608 // exclude if node.isNull, cos that's the point of this.
609 return null;
610 }
611 checkValue(path, node, "the null value");
612 if (!node.isNull()) {
613 throw new IllegalArgumentException(formatExMsg(path, "is not the null value"));
614 }
615 return new JsonRepresentation(node);
616 }
617
618 // ///////////////////////////////////////////////////////////////////////
619 // mapValueAsLink
620 // ///////////////////////////////////////////////////////////////////////
621
622 /**
623 * Convert a representation that contains a single node representing a link
624 * into a {@link LinkRepresentation}.
625 */
626 public LinkRepresentation mapValueAsLink() {
627 if (asJsonNode().size() != 1) {
628 throw new IllegalStateException("does not represent link");
629 }
630 final String linkPropertyName = asJsonNode().getFieldNames().next();
631 return getLink(linkPropertyName);
632 }
633
634 // ///////////////////////////////////////////////////////////////////////
635 // asInputStream
636 // ///////////////////////////////////////////////////////////////////////
637
638 public InputStream asInputStream() {
639 return JsonNodeUtils.asInputStream(jsonNode);
640 }
641
642 // ///////////////////////////////////////////////////////////////////////
643 // asArrayNode, asObjectNode
644 // ///////////////////////////////////////////////////////////////////////
645
646 /**
647 * Convert underlying representation into an array.
648 */
649 protected ArrayNode asArrayNode() {
650 if (!isArray()) {
651 throw new IllegalStateException("does not represent array");
652 }
653 return (ArrayNode) asJsonNode();
654 }
655
656 /**
657 * Convert underlying representation into an object (map).
658 */
659 protected ObjectNode asObjectNode() {
660 if (!isMap()) {
661 throw new IllegalStateException("does not represent map");
662 }
663 return (ObjectNode) asJsonNode();
664 }
665
666 // ///////////////////////////////////////////////////////////////////////
667 // asT
668 // ///////////////////////////////////////////////////////////////////////
669
670 /**
671 * Convenience to simply "downcast".
672 *
673 * <p>
674 * In fact, the method creates a new instance of the specified type, which
675 * shares the underlying {@link #jsonNode jsonNode}.
676 */
677 public <T extends JsonRepresentation> T as(final Class<T> cls) {
678 try {
679 final Constructor<T> constructor = cls.getConstructor(JsonNode.class);
680 return constructor.newInstance(jsonNode);
681 } catch (final Exception e) {
682 throw new RuntimeException(e);
683 }
684 }
685
686 // ///////////////////////////////////////////////////////////////////////
687 // asUrlEncoded
688 // ///////////////////////////////////////////////////////////////////////
689
690 public String asUrlEncoded() {
691 return UrlEncodingUtils.urlEncode(asJsonNode());
692 }
693
694 // ///////////////////////////////////////////////////////////////////////
695 // mutable (array)
696 // ///////////////////////////////////////////////////////////////////////
697
698 public void arrayAdd(final Object value) {
699 if (!isArray()) {
700 throw new IllegalStateException("does not represent array");
701 }
702 asArrayNode().add(new POJONode(value));
703 }
704
705 public void arrayAdd(final JsonRepresentation value) {
706 if (!isArray()) {
707 throw new IllegalStateException("does not represent array");
708 }
709 asArrayNode().add(value.asJsonNode());
710 }
711
712 public void arrayAdd(final String value) {
713 if (!isArray()) {
714 throw new IllegalStateException("does not represent array");
715 }
716 asArrayNode().add(value);
717 }
718
719 public void arrayAdd(final JsonNode value) {
720 if (!isArray()) {
721 throw new IllegalStateException("does not represent array");
722 }
723 asArrayNode().add(value);
724 }
725
726 public void arrayAdd(final long value) {
727 if (!isArray()) {
728 throw new IllegalStateException("does not represent array");
729 }
730 asArrayNode().add(value);
731 }
732
733 public void arrayAdd(final int value) {
734 if (!isArray()) {
735 throw new IllegalStateException("does not represent array");
736 }
737 asArrayNode().add(value);
738 }
739
740 public void arrayAdd(final double value) {
741 if (!isArray()) {
742 throw new IllegalStateException("does not represent array");
743 }
744 asArrayNode().add(value);
745 }
746
747 public void arrayAdd(final float value) {
748 if (!isArray()) {
749 throw new IllegalStateException("does not represent array");
750 }
751 asArrayNode().add(value);
752 }
753
754 public void arrayAdd(final boolean value) {
755 if (!isArray()) {
756 throw new IllegalStateException("does not represent array");
757 }
758 asArrayNode().add(value);
759 }
760
761 public Iterable<JsonRepresentation> arrayIterable() {
762 return arrayIterable(JsonRepresentation.class);
763 }
764
765 public <T> Iterable<T> arrayIterable(final Class<T> requiredType) {
766 return new Iterable<T>() {
767 @Override
768 public Iterator<T> iterator() {
769 return arrayIterator(requiredType);
770 }
771 };
772 }
773
774 public Iterator<JsonRepresentation> arrayIterator() {
775 return arrayIterator(JsonRepresentation.class);
776 }
777
778 public <T> Iterator<T> arrayIterator(final Class<T> requiredType) {
779 ensureIsAnArrayAtLeastAsLargeAs(0);
780 final Function<JsonNode, ?> transformer = representationInstantiatorFor(requiredType);
781 final ArrayNode arrayNode = (ArrayNode) jsonNode;
782 final Iterator<JsonNode> iterator = arrayNode.iterator();
783 final Function<JsonNode, T> typedTransformer = asT(transformer); // necessary
784 // to
785 // do
786 // in
787 // two
788 // steps
789 return Iterators.transform(iterator, typedTransformer);
790 }
791
792 @SuppressWarnings("unchecked")
793 private static <T> Function<JsonNode, T> asT(final Function<JsonNode, ?> transformer) {
794 return (Function<JsonNode, T>) transformer;
795 }
796
797 public JsonRepresentation arrayGet(final int i) {
798 ensureIsAnArrayAtLeastAsLargeAs(i);
799 return new JsonRepresentation(jsonNode.get(i));
800 }
801
802 public void arraySetElementAt(final int i, final JsonRepresentation objectRepr) {
803 ensureIsAnArrayAtLeastAsLargeAs(i);
804 if (objectRepr.isArray()) {
805 throw new IllegalArgumentException("Representation being set cannot be an array");
806 }
807 // can safely downcast because *this* representation is an array
808 final ArrayNode arrayNode = (ArrayNode) jsonNode;
809 arrayNode.set(i, objectRepr.asJsonNode());
810 }
811
812 private void ensureIsAnArrayAtLeastAsLargeAs(final int i) {
813 if (!jsonNode.isArray()) {
814 throw new IllegalStateException("Is not an array");
815 }
816 if (i >= size()) {
817 throw new IndexOutOfBoundsException("array has " + size() + " elements");
818 }
819 }
820
821 // ///////////////////////////////////////////////////////////////////////
822 // mutable (map)
823 // ///////////////////////////////////////////////////////////////////////
824
825 public boolean mapHas(final String key) {
826 if (!isMap()) {
827 throw new IllegalStateException("does not represent map");
828 }
829 ObjectNode node = asObjectNode();
830
831 final String[] paths = key.split("\\.");
832 for (int i = 0; i < paths.length; i++) {
833 final String path = paths[i];
834 final boolean has = node.has(path);
835 if (!has) {
836 return false;
837 }
838 if (i + 1 < paths.length) {
839 // not on last
840 final JsonNode subNode = node.get(path);
841 if (!subNode.isObject()) {
842 return false;
843 }
844 node = (ObjectNode) subNode;
845 }
846 }
847 return true;
848 }
849
850 public void mapPut(final String key, final List<Object> value) {
851 if (!isMap()) {
852 throw new IllegalStateException("does not represent map");
853 }
854 if (value == null) {
855 return;
856 }
857 final JsonRepresentation array = JsonRepresentation.newArray();
858 for (final Object v : value) {
859 array.arrayAdd(v);
860 }
861 mapPut(key, array);
862 }
863
864 public void mapPut(final String key, final Object value) {
865 if (!isMap()) {
866 throw new IllegalStateException("does not represent map");
867 }
868 if (value == null) {
869 return;
870 }
871 final Path path = Path.parse(key);
872 final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead());
873 node.put(path.getTail(), new POJONode(value));
874 }
875
876 public void mapPut(final String key, final JsonRepresentation value) {
877 if (!isMap()) {
878 throw new IllegalStateException("does not represent map");
879 }
880 if (value == null) {
881 return;
882 }
883 final Path path = Path.parse(key);
884 final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead());
885 node.put(path.getTail(), value.asJsonNode());
886 }
887
888 public void mapPut(final String key, final String value) {
889 if (!isMap()) {
890 throw new IllegalStateException("does not represent map");
891 }
892 if (value == null) {
893 return;
894 }
895 final Path path = Path.parse(key);
896 final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead());
897 node.put(path.getTail(), value);
898 }
899
900 public void mapPut(final String key, final JsonNode value) {
901 if (!isMap()) {
902 throw new IllegalStateException("does not represent map");
903 }
904 if (value == null) {
905 return;
906 }
907 final Path path = Path.parse(key);
908 final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead());
909 node.put(path.getTail(), value);
910 }
911
912 public void mapPut(final String key, final long value) {
913 if (!isMap()) {
914 throw new IllegalStateException("does not represent map");
915 }
916 final Path path = Path.parse(key);
917 final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead());
918 node.put(path.getTail(), value);
919 }
920
921 public void mapPut(final String key, final int value) {
922 if (!isMap()) {
923 throw new IllegalStateException("does not represent map");
924 }
925 final Path path = Path.parse(key);
926 final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead());
927 node.put(path.getTail(), value);
928 }
929
930 public void mapPut(final String key, final double value) {
931 if (!isMap()) {
932 throw new IllegalStateException("does not represent map");
933 }
934 final Path path = Path.parse(key);
935 final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead());
936 node.put(path.getTail(), value);
937 }
938
939 public void mapPut(final String key, final float value) {
940 if (!isMap()) {
941 throw new IllegalStateException("does not represent map");
942 }
943 final Path path = Path.parse(key);
944 final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead());
945 node.put(path.getTail(), value);
946 }
947
948 public void mapPut(final String key, final boolean value) {
949 if (!isMap()) {
950 throw new IllegalStateException("does not represent map");
951 }
952 final Path path = Path.parse(key);
953 final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead());
954 node.put(path.getTail(), value);
955 }
956
957 private static class Path {
958 private final List<String> head;
959 private final String tail;
960
961 private Path(final List<String> head, final String tail) {
962 this.head = Collections.unmodifiableList(head);
963 this.tail = tail;
964 }
965
966 public List<String> getHead() {
967 return head;
968 }
969
970 public String getTail() {
971 return tail;
972 }
973
974 public static Path parse(final String pathStr) {
975 final List<String> keyList = Lists.newArrayList(Arrays.asList(pathStr.split("\\.")));
976 if (keyList.size() == 0) {
977 throw new IllegalArgumentException(String.format("Malformed path '%s'", pathStr));
978 }
979 final String tail = keyList.remove(keyList.size() - 1);
980 return new Path(keyList, tail);
981 }
982 }
983
984 public Iterable<Map.Entry<String, JsonRepresentation>> mapIterable() {
985 ensureIsAMap();
986 return new Iterable<Map.Entry<String, JsonRepresentation>>() {
987
988 @Override
989 public Iterator<Entry<String, JsonRepresentation>> iterator() {
990 return mapIterator();
991 }
992 };
993 }
994
995 public Iterator<Map.Entry<String, JsonRepresentation>> mapIterator() {
996 ensureIsAMap();
997 return Iterators.transform(jsonNode.getFields(), MAP_ENTRY_JSON_NODE_TO_JSON_REPRESENTATION);
998 }
999
1000 private void ensureIsAMap() {
1001 if (!jsonNode.isObject()) {
1002 throw new IllegalStateException("Is not a map");
1003 }
1004 }
1005
1006 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>>() {
1007
1008 @Override
1009 public Entry<String, JsonRepresentation> apply(final Entry<String, JsonNode> input) {
1010 return new Map.Entry<String, JsonRepresentation>() {
1011
1012 @Override
1013 public String getKey() {
1014 return input.getKey();
1015 }
1016
1017 @Override
1018 public JsonRepresentation getValue() {
1019 return new JsonRepresentation(input.getValue());
1020 }
1021
1022 @Override
1023 public JsonRepresentation setValue(final JsonRepresentation value) {
1024 final JsonNode setValue = input.setValue(value.asJsonNode());
1025 return new JsonRepresentation(setValue);
1026 }
1027 };
1028 }
1029 };
1030
1031 // ///////////////////////////////////////////////////////////////////////
1032 // helpers
1033 // ///////////////////////////////////////////////////////////////////////
1034
1035 /**
1036 * A reciprocal of the behaviour of the automatic dereferencing of arrays
1037 * that occurs when there is only a single instance.
1038 *
1039 * @see #toJsonNode(List)
1040 */
1041 public JsonRepresentation ensureArray() {
1042 if (jsonNode.isArray()) {
1043 return this;
1044 }
1045 final JsonRepresentation arrayRepr = JsonRepresentation.newArray();
1046 arrayRepr.arrayAdd(jsonNode);
1047 return arrayRepr;
1048 }
1049
1050 private JsonNode getNode(final String path) {
1051 JsonNode jsonNode = this.jsonNode;
1052 final String[] keys = path.split("\\.");
1053 for (final String key : keys) {
1054 final PathNode pathNode = PathNode.parse(key);
1055 if (!pathNode.getKey().isEmpty()) {
1056 jsonNode = jsonNode.path(pathNode.getKey());
1057 } else {
1058 // pathNode is criteria only; don't change jsonNode
1059 }
1060 if (jsonNode.isNull()) {
1061 return jsonNode;
1062 }
1063 if (!pathNode.hasCriteria()) {
1064 continue;
1065 }
1066 if (!jsonNode.isArray()) {
1067 return NullNode.getInstance();
1068 }
1069 jsonNode = matching(jsonNode, pathNode);
1070 if (jsonNode.isNull()) {
1071 return jsonNode;
1072 }
1073 }
1074 return jsonNode;
1075 }
1076
1077 private JsonNode matching(final JsonNode jsonNode, final PathNode pathNode) {
1078 final JsonRepresentation asList = new JsonRepresentation(jsonNode);
1079 final Iterable<JsonNode> filtered = Iterables.filter(asList.arrayIterable(JsonNode.class), new Predicate<JsonNode>() {
1080 @Override
1081 public boolean apply(final JsonNode input) {
1082 return pathNode.matches(new JsonRepresentation(input));
1083 }
1084 });
1085 final List<JsonNode> matching = Lists.newArrayList(filtered);
1086 return toJsonNode(matching);
1087 }
1088
1089 private static JsonNode toJsonNode(final List<JsonNode> matching) {
1090 switch (matching.size()) {
1091 case 0:
1092 return NullNode.getInstance();
1093 case 1:
1094 return matching.get(0);
1095 default:
1096 final ArrayNode arrayNode = new ArrayNode(JsonNodeFactory.instance);
1097 arrayNode.addAll(matching);
1098 return arrayNode;
1099 }
1100 }
1101
1102 private static void checkValue(final String path, final JsonNode node, final String requiredType) {
1103 if (node.isValueNode()) {
1104 return;
1105 }
1106 throw new IllegalArgumentException(formatExMsg(path, "is not " + requiredType));
1107 }
1108
1109 private static boolean representsNull(final JsonNode node) {
1110 return node == null || node.isMissingNode() || node.isNull();
1111 }
1112
1113 private static String formatExMsg(final String pathIfAny, final String errorText) {
1114 final StringBuilder buf = new StringBuilder();
1115 if (pathIfAny != null) {
1116 buf.append("'").append(pathIfAny).append("' ");
1117 }
1118 buf.append(errorText);
1119 return buf.toString();
1120 }
1121
1122 // ///////////////////////////////////////////////////////////////////////
1123 // toString
1124 // ///////////////////////////////////////////////////////////////////////
1125
1126 @Override
1127 public String toString() {
1128 return jsonNode.toString();
1129 }
1130
1131 }