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 &quot;downcast&quot;.
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}