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.*;
026import java.util.Map.Entry;
027import java.util.regex.Matcher;
028import java.util.regex.Pattern;
029import com.google.common.base.Function;
030import com.google.common.base.Predicate;
031import com.google.common.collect.Iterables;
032import com.google.common.collect.Iterators;
033import com.google.common.collect.Lists;
034import com.google.common.collect.Maps;
035import org.codehaus.jackson.JsonNode;
036import org.codehaus.jackson.node.*;
037import org.joda.time.LocalTime;
038import org.joda.time.format.DateTimeFormatter;
039import org.joda.time.format.ISODateTimeFormat;
040import org.apache.isis.viewer.restfulobjects.applib.util.JsonNodeUtils;
041import org.apache.isis.viewer.restfulobjects.applib.util.PathNode;
042import org.apache.isis.viewer.restfulobjects.applib.util.UrlEncodingUtils;
043
044/**
045 * A wrapper around {@link JsonNode} that provides some additional helper
046 * methods.
047 */
048public class JsonRepresentation {
049
050    private static final Pattern FORMAT_BIG_DECIMAL = Pattern.compile("big-decimal\\((\\d+),(\\d+)\\)");
051    private static final Pattern FORMAT_BIG_INTEGER = Pattern.compile("big-integer\\((\\d+)\\)");
052
053    public interface HasLinkToSelf {
054        public LinkRepresentation getSelf();
055    }
056
057    public interface HasLinkToUp {
058        public LinkRepresentation getUp();
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    // isNumber
259    // ///////////////////////////////////////////////////////////////////////
260
261    public boolean isNumber(final String path) {
262        return isNumber(getNode(path));
263    }
264
265    public boolean isNumber() {
266        return isNumber(asJsonNode());
267    }
268
269    private boolean isNumber(final JsonNode node) {
270        return !representsNull(node) && node.isValueNode() && node.isNumber();
271    }
272
273    public Number asNumber() {
274        return getNumber(null, asJsonNode());
275    }
276
277    private Number getNumber(final String path, final JsonNode node) {
278        if (representsNull(node)) {
279            return null;
280        }
281        checkValue(path, node, "a number");
282        if (!node.isNumber()) {
283            throw new IllegalArgumentException(formatExMsg(path, "is not a number"));
284        }
285        return node.getNumberValue();
286    }
287
288
289    // ///////////////////////////////////////////////////////////////////////
290    // isIntegralNumber, getIntegralNumber, asIntegralNumber
291    // ///////////////////////////////////////////////////////////////////////
292
293    /**
294     * Is a long, an int or a {@link BigInteger}.
295     */
296    public boolean isIntegralNumber(final String path) {
297        return isIntegralNumber(getNode(path));
298    }
299
300    /**
301     * Is a long, an int or a {@link BigInteger}.
302     */
303    public boolean isIntegralNumber() {
304        return isIntegralNumber(asJsonNode());
305    }
306
307    private boolean isIntegralNumber(final JsonNode node) {
308        return !representsNull(node) && node.isValueNode() && node.isIntegralNumber();
309    }
310
311
312    // ///////////////////////////////////////////////////////////////////////
313    // getDate, asDate
314    // ///////////////////////////////////////////////////////////////////////
315
316    public final static DateTimeFormatter yyyyMMdd = ISODateTimeFormat.date().withZoneUTC();
317
318    public java.util.Date getDate(final String path) {
319        return getDate(path, getNode(path));
320    }
321
322    public java.util.Date asDate() {
323        return getDate(null, asJsonNode());
324    }
325
326    private java.util.Date getDate(final String path, final JsonNode node) {
327        if (representsNull(node)) {
328            return null;
329        }
330        checkValue(path, node, "a date");
331        if (!node.isTextual()) {
332            throw new IllegalArgumentException(formatExMsg(path, "is not a date"));
333        }
334        final String textValue = node.getTextValue();
335        return new java.util.Date(yyyyMMdd.parseMillis(textValue));
336    }
337
338    // ///////////////////////////////////////////////////////////////////////
339    // getDateTime, asDateTime
340    // ///////////////////////////////////////////////////////////////////////
341
342    public final static DateTimeFormatter yyyyMMddTHHmmssZ = ISODateTimeFormat.dateTimeNoMillis().withZoneUTC();
343
344    public java.util.Date getDateTime(final String path) {
345        return getDateTime(path, getNode(path));
346    }
347
348    public java.util.Date asDateTime() {
349        return getDateTime(null, asJsonNode());
350    }
351
352    private java.util.Date getDateTime(final String path, final JsonNode node) {
353        if (representsNull(node)) {
354            return null;
355        }
356        checkValue(path, node, "a date-time");
357        if (!node.isTextual()) {
358            throw new IllegalArgumentException(formatExMsg(path, "is not a date-time"));
359        }
360        final String textValue = node.getTextValue();
361        return new java.util.Date(yyyyMMddTHHmmssZ.parseMillis(textValue));
362    }
363
364    // ///////////////////////////////////////////////////////////////////////
365    // getTime, asTime
366    // ///////////////////////////////////////////////////////////////////////
367
368    public final static DateTimeFormatter _HHmmss = ISODateTimeFormat.timeNoMillis().withZoneUTC();
369
370    public java.util.Date getTime(final String path) {
371        return getTime(path, getNode(path));
372    }
373
374    public java.util.Date asTime() {
375        return getTime(null, asJsonNode());
376    }
377
378    private java.util.Date getTime(final String path, final JsonNode node) {
379        if (representsNull(node)) {
380            return null;
381        }
382        checkValue(path, node, "a time");
383        if (!node.isTextual()) {
384            throw new IllegalArgumentException(formatExMsg(path, "is not a time"));
385        }
386        final String textValue = node.getTextValue();
387        final LocalTime localTime = _HHmmss.parseLocalTime(textValue + "Z");
388        return new java.util.Date(localTime.getMillisOfDay());
389    }
390
391    // ///////////////////////////////////////////////////////////////////////
392    // isBoolean, getBoolean, asBoolean
393    // ///////////////////////////////////////////////////////////////////////
394    
395    public boolean isBoolean(final String path) {
396        return isBoolean(getNode(path));
397    }
398    
399    public boolean isBoolean() {
400        return isBoolean(asJsonNode());
401    }
402    
403    private boolean isBoolean(final JsonNode node) {
404        return !representsNull(node) && node.isValueNode() && node.isBoolean();
405    }
406    
407    /**
408     * Use {@link #isBoolean(String)} to check first, if required.
409     */
410    public Boolean getBoolean(final String path) {
411        return getBoolean(path, getNode(path));
412    }
413    
414    /**
415     * Use {@link #isBoolean()} to check first, if required.
416     */
417    public Boolean asBoolean() {
418        return getBoolean(null, asJsonNode());
419    }
420    
421    private Boolean getBoolean(final String path, final JsonNode node) {
422        if (representsNull(node)) {
423            return null;
424        }
425        checkValue(path, node, "a boolean");
426        if (!node.isBoolean()) {
427            throw new IllegalArgumentException(formatExMsg(path, "is not a boolean"));
428        }
429        return node.getBooleanValue();
430    }
431    
432    // ///////////////////////////////////////////////////////////////////////
433    // isByte, getByte, asByte
434    // ///////////////////////////////////////////////////////////////////////
435
436    /**
437     * Use {@link #isIntegralNumber(String)} to test if number (it is not possible to check if a byte, however).
438     */
439    public Byte getByte(final String path) {
440        final JsonNode node = getNode(path);
441        return getByte(path, node);
442    }
443    
444    /**
445     * Use {@link #isIntegralNumber()} to test if number (it is not possible to check if a byte, however).
446     */
447    public Byte asByte() {
448        return getByte(null, asJsonNode());
449    }
450    
451    private Byte getByte(final String path, final JsonNode node) {
452        if (representsNull(node)) {
453            return null;
454        }
455        checkValue(path, node, "an byte");
456        if (!node.isNumber()) {
457            // there is no node.isByte()
458            throw new IllegalArgumentException(formatExMsg(path, "is not a number"));
459        }
460        return node.getNumberValue().byteValue();
461    }
462
463    // ///////////////////////////////////////////////////////////////////////
464    // getShort, asShort
465    // ///////////////////////////////////////////////////////////////////////
466
467    /**
468     * Use {@link #isIntegralNumber(String)} to check if number (it is not possible to check if a short, however).
469     */
470    public Short getShort(final String path) {
471        final JsonNode node = getNode(path);
472        return getShort(path, node);
473    }
474
475    /**
476     * Use {@link #isIntegralNumber()} to check if number (it is not possible to check if a short, however).
477     */
478    public Short asShort() {
479        return getShort(null, asJsonNode());
480    }
481    
482    private Short getShort(final String path, final JsonNode node) {
483        if (representsNull(node)) {
484            return null;
485        }
486        checkValue(path, node, "an short");
487        if (!node.isNumber()) {
488            // there is no node.isShort()
489            throw new IllegalArgumentException(formatExMsg(path, "is not a number"));
490        }
491        return node.getNumberValue().shortValue();
492    }
493    
494
495    // ///////////////////////////////////////////////////////////////////////
496    // getChar, asChar
497    // ///////////////////////////////////////////////////////////////////////
498
499    /**
500     * Use {@link #isString(String)} to check if string (it is not possible to check if a character, however).
501     */
502    public Character getChar(final String path) {
503        final JsonNode node = getNode(path);
504        return getChar(path, node);
505    }
506
507    /**
508     * Use {@link #isString()} to check if string (it is not possible to check if a character, however).
509     */
510    public Character asChar() {
511        return getChar(null, asJsonNode());
512    }
513    
514    private Character getChar(final String path, final JsonNode node) {
515        if (representsNull(node)) {
516            return null;
517        }
518        checkValue(path, node, "an short");
519        if (!node.isTextual()) {
520            throw new IllegalArgumentException(formatExMsg(path, "is not textual"));
521        }
522        final String textValue = node.getTextValue();
523        if(textValue == null || textValue.length() == 0) {
524            return null;
525        }
526        return textValue.charAt(0);
527    }
528    
529
530    // ///////////////////////////////////////////////////////////////////////
531    // isInt, getInt, asInt
532    // ///////////////////////////////////////////////////////////////////////
533
534    public boolean isInt(final String path) {
535        return isInt(getNode(path));
536    }
537
538    public boolean isInt() {
539        return isInt(asJsonNode());
540    }
541
542    private boolean isInt(final JsonNode node) {
543        return !representsNull(node) && node.isValueNode() && node.isInt();
544    }
545
546    /**
547     * Use {@link #isInt(String)} to check first, if required.
548     */
549    public Integer getInt(final String path) {
550        final JsonNode node = getNode(path);
551        return getInt(path, node);
552    }
553
554    /**
555     * Use {@link #isInt()} to check first, if required.
556     */
557    public Integer asInt() {
558        return getInt(null, asJsonNode());
559    }
560
561    private Integer getInt(final String path, final JsonNode node) {
562        if (representsNull(node)) {
563            return null;
564        }
565        checkValue(path, node, "an int");
566        if (!node.isInt()) {
567            throw new IllegalArgumentException(formatExMsg(path, "is not an int"));
568        }
569        return node.getIntValue();
570    }
571
572
573    // ///////////////////////////////////////////////////////////////////////
574    // isLong, getLong, asLong
575    // ///////////////////////////////////////////////////////////////////////
576
577    public boolean isLong(final String path) {
578        return isLong(getNode(path));
579    }
580
581    public boolean isLong() {
582        return isLong(asJsonNode());
583    }
584
585    private boolean isLong(final JsonNode node) {
586        return !representsNull(node) && node.isValueNode() && node.isLong();
587    }
588
589    /**
590     * Use {@link #isLong(String)} to check first, if required.
591     */
592    public Long getLong(final String path) {
593        final JsonNode node = getNode(path);
594        return getLong(path, node);
595    }
596
597    /**
598     * Use {@link #isLong()} to check first, if required.
599     */
600    public Long asLong() {
601        return getLong(null, asJsonNode());
602    }
603
604    private Long getLong(final String path, final JsonNode node) {
605        if (representsNull(node)) {
606            return null;
607        }
608        checkValue(path, node, "a long");
609        if(node.isInt()) {
610            return Long.valueOf(node.getIntValue());
611        }
612        if(node.isLong()) {
613            return node.getLongValue();
614        }
615        throw new IllegalArgumentException(formatExMsg(path, "is not a long"));
616    }
617
618    // ///////////////////////////////////////////////////////////////////////
619    // getFloat, asFloat
620    // ///////////////////////////////////////////////////////////////////////
621
622    /**
623     * Use {@link #isDecimal(String)} to test if a decimal value
624     */
625    public Float getFloat(final String path) {
626        final JsonNode node = getNode(path);
627        return getFloat(path, node);
628    }
629    
630    /**
631     * Use {@link #isNumber()} to test if number (it is not possible to check if a float, however).
632     */
633    public Float asFloat() {
634        return getFloat(null, asJsonNode());
635    }
636    
637    private Float getFloat(final String path, final JsonNode node) {
638        if (representsNull(node)) {
639            return null;
640        }
641        checkValue(path, node, "a float");
642        if (!node.isNumber()) {
643            throw new IllegalArgumentException(formatExMsg(path, "is not a number"));
644        }
645        return node.getNumberValue().floatValue();
646    }
647    
648
649    // ///////////////////////////////////////////////////////////////////////
650    // isDecimal, isDouble, getDouble, asDouble
651    // ///////////////////////////////////////////////////////////////////////
652
653    public boolean isDecimal(final String path) {
654        return isDecimal(getNode(path));
655    }
656
657    public boolean isDecimal() {
658        return isDecimal(asJsonNode());
659    }
660
661    /**
662     * @deprecated - use {@link #isDecimal(String)}
663     */
664    @Deprecated
665    public boolean isDouble(final String path) {
666        return isDecimal(path);
667    }
668
669    /**
670     * @deprecated - use {@link #isDecimal()}
671     */
672    @Deprecated
673    public boolean isDouble() {
674        return isDecimal();
675    }
676
677    private boolean isDecimal(final JsonNode node) {
678        return !representsNull(node) && node.isValueNode() && node.isDouble();
679    }
680
681    /**
682     * Use {@link #isDouble(String)} to check first, if required.
683     */
684    public Double getDouble(final String path) {
685        final JsonNode node = getNode(path);
686        return getDouble(path, node);
687    }
688
689    /**
690     * Use {@link #isDouble()} to check first, if required.
691     */
692    public Double asDouble() {
693        return getDouble(null, asJsonNode());
694    }
695
696    private Double getDouble(final String path, final JsonNode node) {
697        if (representsNull(node)) {
698            return null;
699        }
700        checkValue(path, node, "a double");
701        if (!node.isDouble()) {
702            throw new IllegalArgumentException(formatExMsg(path, "is not a double"));
703        }
704        return node.getDoubleValue();
705    }
706
707    // ///////////////////////////////////////////////////////////////////////
708    // isBigInteger, getBigInteger, asBigInteger
709    // ///////////////////////////////////////////////////////////////////////
710
711    public boolean isBigInteger(final String path) {
712        return isBigInteger(getNode(path));
713    }
714
715    public boolean isBigInteger() {
716        return isBigInteger(asJsonNode());
717    }
718
719    private boolean isBigInteger(final JsonNode node) {
720        return !representsNull(node) && node.isValueNode() && (node.isBigInteger() || node.isLong() || node.isInt() || node.isTextual() && parseableAsBigInteger(node.getTextValue()));
721    }
722
723    private static boolean parseableAsBigInteger(String str) {
724        try {
725            new BigInteger(str);
726            return true;
727        } catch (Exception e) {
728            return false;
729        }
730    }
731
732    /**
733     * Use {@link #isBigInteger(String)} to check first, if required.
734     */
735    public BigInteger getBigInteger(final String path) {
736        return getBigInteger(path, (String)null);
737    }
738
739    /**
740     * Use {@link #isBigInteger(String)} to check first, if required.
741     */
742    public BigInteger getBigInteger(final String path, final String formatRequested) {
743        final JsonNode node;
744        final String format;
745        if(formatRequested != null) {
746            node = getNode(path);
747            format = formatRequested;
748        } else {
749            final NodeAndFormat nodeAndFormat = getNodeAndFormat(path);
750            node = nodeAndFormat.node;
751            format = nodeAndFormat.format;
752        }
753        return getBigInteger(path, format, node);
754    }
755
756    /**
757     * Use {@link #isBigInteger()} to check first, if required.
758     */
759    public BigInteger asBigInteger() {
760        return asBigInteger(null);
761    }
762
763    public BigInteger asBigInteger(final String format) {
764        return getBigInteger(null, format, asJsonNode());
765    }
766
767    private BigInteger getBigInteger(final String path, final String format, final JsonNode node) {
768        if (representsNull(node)) {
769            return null;
770        }
771        final String requiredType = "a biginteger";
772        if(!isBigInteger(node)) {
773            throw new IllegalArgumentException(formatExMsg(path, "is not " + requiredType));
774        }
775        checkValue(path, node, requiredType);
776        final BigInteger bigInteger = getBigInteger(path, node);
777        if(format != null) {
778            final Matcher matcher = FORMAT_BIG_INTEGER.matcher(format);
779            if(matcher.matches()) {
780                final int precision = Integer.parseInt(matcher.group(1));
781                final BigInteger maxAllowed = BigInteger.TEN.pow(precision);
782                if(bigInteger.compareTo(maxAllowed) > 0) {
783                    throw new IllegalArgumentException(String.format("Value '%s' larger than that allowed by format '%s'", bigInteger, format));
784                }
785            }
786        }
787        return bigInteger;
788    }
789
790    private BigInteger getBigInteger(String path, JsonNode node) {
791        if (node.isBigInteger()) {
792            return node.getBigIntegerValue();
793        }
794        if (node.isTextual()) {
795            return new BigInteger(node.getTextValue());
796        }
797        if (node.isLong()) {
798            return BigInteger.valueOf(node.getLongValue());
799        }
800        if (node.isInt()) {
801            return BigInteger.valueOf(node.getIntValue());
802        }
803        throw new IllegalArgumentException(formatExMsg(path, "is not a biginteger, is not any other integral number, is not text parseable as a biginteger"));
804    }
805
806    // ///////////////////////////////////////////////////////////////////////
807    // isBigDecimal, getBigDecimal, asBigDecimal
808    // ///////////////////////////////////////////////////////////////////////
809
810    public boolean isBigDecimal(final String path) {
811        return isBigDecimal(getNode(path));
812    }
813
814    public boolean isBigDecimal() {
815        return isBigDecimal(asJsonNode());
816    }
817
818    private boolean isBigDecimal(final JsonNode node) {
819        return !representsNull(node) && node.isValueNode() && (node.isBigDecimal() || node.isDouble() || node.isLong() || node.isInt() || node.isBigInteger() || node.isTextual() && parseableAsBigDecimal(node.getTextValue()));
820    }
821
822    private static boolean parseableAsBigDecimal(String str) {
823        try {
824            new BigDecimal(str);
825            return true;
826        } catch (Exception e) {
827            return false;
828        }
829    }
830
831    /**
832     * Use {@link #isBigDecimal(String)} to check first, if required.
833     */
834    public BigDecimal getBigDecimal(final String path) {
835        return getBigDecimal(path, (String)null);
836    }
837
838    /**
839     * Use {@link #isBigDecimal(String)} to check first, if required.
840     */
841    public BigDecimal getBigDecimal(final String path, final String formatRequested) {
842        final JsonNode node;
843        final String format;
844        if(formatRequested != null) {
845            node = getNode(path);
846            format = formatRequested;
847        } else {
848            final NodeAndFormat nodeAndFormat = getNodeAndFormat(path);
849            node = nodeAndFormat.node;
850            format = nodeAndFormat.format;
851        }
852        return getBigDecimal(path, format, node);
853    }
854
855    /**
856     * Use {@link #isBigDecimal()} to check first, if required.
857     */
858    public BigDecimal asBigDecimal() {
859        return asBigDecimal(null);
860    }
861
862    /**
863     * Use {@link #isBigDecimal()} to check first, if required.
864     */
865    public BigDecimal asBigDecimal(String format) {
866        return getBigDecimal(null, format, asJsonNode());
867    }
868
869    private BigDecimal getBigDecimal(final String path, final String format, final JsonNode node) {
870        if (representsNull(node)) {
871            return null;
872        }
873        final String requiredType = "a bigdecimal";
874        if(!isBigDecimal(node)) {
875            throw new IllegalArgumentException(formatExMsg(path, "is not " + requiredType));
876        }
877        checkValue(path, node, requiredType);
878        final BigDecimal bigDecimal = getBigDecimal(path, node);
879        if(format != null) {
880            final Matcher matcher = FORMAT_BIG_DECIMAL.matcher(format);
881            if(matcher.matches()) {
882                final int precision = Integer.parseInt(matcher.group(1));
883                final int scale = Integer.parseInt(matcher.group(2));
884                final BigDecimal maxAllowed = BigDecimal.TEN.pow(precision-scale);
885                if(bigDecimal.compareTo(maxAllowed) > 0) {
886                    throw new IllegalArgumentException(String.format("Value '%s' larger than that allowed by format '%s'", bigDecimal, format));
887                }
888                return bigDecimal.setScale(scale, BigDecimal.ROUND_HALF_EVEN);
889            }
890        }
891        return bigDecimal;
892    }
893
894    private BigDecimal getBigDecimal(String path, JsonNode node) {
895        if (node.isBigDecimal()) {
896            return node.getDecimalValue();
897        }
898        if (node.isTextual()) {
899            return new BigDecimal(node.getTextValue());
900        }
901        if (node.isLong()) {
902            return new BigDecimal(node.getLongValue());
903        }
904        if (node.isDouble()) {
905            // there will be rounding errors, most likely
906            return new BigDecimal(node.getDoubleValue());
907        }
908        if (node.isBigInteger()) {
909            return new BigDecimal(node.getBigIntegerValue());
910        }
911        if (node.isInt()) {
912            return new BigDecimal(node.getIntValue());
913        }
914        throw new IllegalArgumentException(formatExMsg(path, "is not a bigdecimal, is not any other numeric, is not text parseable as a bigdecimal"));
915    }
916
917
918    // ///////////////////////////////////////////////////////////////////////
919    // getString, isString, asString
920    // ///////////////////////////////////////////////////////////////////////
921
922    public boolean isString(final String path) {
923        return isString(getNode(path));
924    }
925
926    public boolean isString() {
927        return isString(asJsonNode());
928    }
929
930    private boolean isString(final JsonNode node) {
931        return !representsNull(node) && node.isValueNode() && node.isTextual();
932    }
933
934    /**
935     * Use {@link #isString(String)} to check first, if required.
936     */
937    public String getString(final String path) {
938        final JsonNode node = getNode(path);
939        return getString(path, node);
940    }
941
942    /**
943     * Use {@link #isString()} to check first, if required.
944     */
945    public String asString() {
946        return getString(null, asJsonNode());
947    }
948
949    private String getString(final String path, final JsonNode node) {
950        if (representsNull(node)) {
951            return null;
952        }
953        checkValue(path, node, "a string");
954        if (!node.isTextual()) {
955            throw new IllegalArgumentException(formatExMsg(path, "is not a string"));
956        }
957        return node.getTextValue();
958    }
959
960    public String asArg() {
961        if (isValue()) {
962            return asJsonNode().getValueAsText();
963        } else {
964            return asJsonNode().toString();
965        }
966    }
967
968    // ///////////////////////////////////////////////////////////////////////
969    // isLink, getLink, asLink
970    // ///////////////////////////////////////////////////////////////////////
971
972    public boolean isLink() {
973        return isLink(asJsonNode());
974    }
975
976    public boolean isLink(final String path) {
977        return isLink(getNode(path));
978    }
979
980    public boolean isLink(final JsonNode node) {
981        if (representsNull(node) || isArray(node) || node.isValueNode()) {
982            return false;
983        }
984
985        final LinkRepresentation link = new LinkRepresentation(node);
986        if (link.getHref() == null) {
987            return false;
988        }
989        return true;
990    }
991
992    /**
993     * Use {@link #isLink(String)} to check first, if required.
994     */
995    public LinkRepresentation getLink(final String path) {
996        return getLink(path, getNode(path));
997    }
998
999    /**
1000     * Use {@link #isLink()} to check first, if required.
1001     */
1002    public LinkRepresentation asLink() {
1003        return getLink(null, asJsonNode());
1004    }
1005
1006    private LinkRepresentation getLink(final String path, final JsonNode node) {
1007        if (representsNull(node)) {
1008            return null;
1009        }
1010
1011        if (isArray(node)) {
1012            throw new IllegalArgumentException(formatExMsg(path, "is an array that does not represent a link"));
1013        }
1014        if (node.isValueNode()) {
1015            throw new IllegalArgumentException(formatExMsg(path, "is a value that does not represent a link"));
1016        }
1017
1018        final LinkRepresentation link = new LinkRepresentation(node);
1019        if (link.getHref() == null) {
1020            throw new IllegalArgumentException(formatExMsg(path, "is a map that does not fully represent a link"));
1021        }
1022        return link;
1023    }
1024
1025    // ///////////////////////////////////////////////////////////////////////
1026    // getNull, isNull
1027    // ///////////////////////////////////////////////////////////////////////
1028
1029    public boolean isNull() {
1030        return isNull(asJsonNode());
1031    }
1032
1033    /**
1034     * Indicates that the wrapped node has <tt>null</tt> value (ie
1035     * {@link JsonRepresentation#isNull()}), or returns <tt>null</tt> if there
1036     * was no node with the provided path.
1037     */
1038    public Boolean isNull(final String path) {
1039        return isNull(getNode(path));
1040    }
1041
1042    private Boolean isNull(final JsonNode node) {
1043        if (node == null || node.isMissingNode()) {
1044            // not exclude if node.isNull, cos that's the point of this.
1045            return null;
1046        }
1047        return node.isNull();
1048    }
1049
1050    /**
1051     * Either returns a {@link JsonRepresentation} that indicates that the
1052     * wrapped node has <tt>null</tt> value (ie
1053     * {@link JsonRepresentation#isNull()}), or returns <tt>null</tt> if there
1054     * was no node with the provided path.
1055     * 
1056     * <p>
1057     * Use {@link #isNull(String)} to check first, if required.
1058     */
1059    public JsonRepresentation getNull(final String path) {
1060        return getNull(path, getNode(path));
1061    }
1062
1063    /**
1064     * Either returns a {@link JsonRepresentation} that indicates that the
1065     * wrapped node has <tt>null</tt> value (ie
1066     * {@link JsonRepresentation#isNull()}), or returns <tt>null</tt> if there
1067     * was no node with the provided path.
1068     *
1069     * <p>
1070     * Use {@link #isNull()} to check first, if required.
1071     */
1072    public JsonRepresentation asNull() {
1073        return getNull(null, asJsonNode());
1074    }
1075
1076    private JsonRepresentation getNull(final String path, final JsonNode node) {
1077        if (node == null || node.isMissingNode()) {
1078            // exclude if node.isNull, cos that's the point of this.
1079            return null;
1080        }
1081        checkValue(path, node, "the null value");
1082        if (!node.isNull()) {
1083            throw new IllegalArgumentException(formatExMsg(path, "is not the null value"));
1084        }
1085        return new JsonRepresentation(node);
1086    }
1087
1088    // ///////////////////////////////////////////////////////////////////////
1089    // mapValueAsLink
1090    // ///////////////////////////////////////////////////////////////////////
1091
1092    /**
1093     * Convert a representation that contains a single node representing a link
1094     * into a {@link LinkRepresentation}.
1095     */
1096    public LinkRepresentation mapValueAsLink() {
1097        if (asJsonNode().size() != 1) {
1098            throw new IllegalStateException("does not represent link");
1099        }
1100        final String linkPropertyName = asJsonNode().getFieldNames().next();
1101        return getLink(linkPropertyName);
1102    }
1103
1104    // ///////////////////////////////////////////////////////////////////////
1105    // asInputStream
1106    // ///////////////////////////////////////////////////////////////////////
1107
1108    public InputStream asInputStream() {
1109        return JsonNodeUtils.asInputStream(jsonNode);
1110    }
1111
1112    // ///////////////////////////////////////////////////////////////////////
1113    // asArrayNode, asObjectNode
1114    // ///////////////////////////////////////////////////////////////////////
1115
1116    /**
1117     * Convert underlying representation into an array.
1118     */
1119    protected ArrayNode asArrayNode() {
1120        if (!isArray()) {
1121            throw new IllegalStateException("does not represent array");
1122        }
1123        return (ArrayNode) asJsonNode();
1124    }
1125
1126    /**
1127     * Convert underlying representation into an object (map).
1128     */
1129    protected ObjectNode asObjectNode() {
1130        if (!isMap()) {
1131            throw new IllegalStateException("does not represent map");
1132        }
1133        return (ObjectNode) asJsonNode();
1134    }
1135
1136    // ///////////////////////////////////////////////////////////////////////
1137    // asT
1138    // ///////////////////////////////////////////////////////////////////////
1139
1140    /**
1141     * Convenience to simply &quot;downcast&quot;.
1142     * 
1143     * <p>
1144     * In fact, the method creates a new instance of the specified type, which
1145     * shares the underlying {@link #jsonNode jsonNode}.
1146     */
1147    public <T extends JsonRepresentation> T as(final Class<T> cls) {
1148        try {
1149            final Constructor<T> constructor = cls.getConstructor(JsonNode.class);
1150            return constructor.newInstance(jsonNode);
1151        } catch (final Exception e) {
1152            throw new RuntimeException(e);
1153        }
1154    }
1155
1156    // ///////////////////////////////////////////////////////////////////////
1157    // asUrlEncoded
1158    // ///////////////////////////////////////////////////////////////////////
1159
1160    public String asUrlEncoded() {
1161        return UrlEncodingUtils.urlEncode(asJsonNode());
1162    }
1163
1164    // ///////////////////////////////////////////////////////////////////////
1165    // mutable (array)
1166    // ///////////////////////////////////////////////////////////////////////
1167
1168    public JsonRepresentation arrayAdd(final Object value) {
1169        if (!isArray()) {
1170            throw new IllegalStateException("does not represent array");
1171        }
1172        asArrayNode().add(new POJONode(value));
1173        return this;
1174    }
1175
1176    public JsonRepresentation arrayAdd(final JsonRepresentation value) {
1177        if (!isArray()) {
1178            throw new IllegalStateException("does not represent array");
1179        }
1180        asArrayNode().add(value.asJsonNode());
1181        return this;
1182    }
1183
1184    public JsonRepresentation arrayAdd(final String value) {
1185        if (!isArray()) {
1186            throw new IllegalStateException("does not represent array");
1187        }
1188        asArrayNode().add(value);
1189        return this;
1190    }
1191
1192    public JsonRepresentation arrayAdd(final JsonNode value) {
1193        if (!isArray()) {
1194            throw new IllegalStateException("does not represent array");
1195        }
1196        asArrayNode().add(value);
1197        return this;
1198    }
1199
1200    public JsonRepresentation arrayAdd(final long value) {
1201        if (!isArray()) {
1202            throw new IllegalStateException("does not represent array");
1203        }
1204        asArrayNode().add(value);
1205        return this;
1206    }
1207
1208    public JsonRepresentation arrayAdd(final int value) {
1209        if (!isArray()) {
1210            throw new IllegalStateException("does not represent array");
1211        }
1212        asArrayNode().add(value);
1213        return this;
1214    }
1215
1216    public JsonRepresentation arrayAdd(final double value) {
1217        if (!isArray()) {
1218            throw new IllegalStateException("does not represent array");
1219        }
1220        asArrayNode().add(value);
1221        return this;
1222    }
1223
1224    public JsonRepresentation arrayAdd(final float value) {
1225        if (!isArray()) {
1226            throw new IllegalStateException("does not represent array");
1227        }
1228        asArrayNode().add(value);
1229        return this;
1230    }
1231
1232    public JsonRepresentation arrayAdd(final boolean value) {
1233        if (!isArray()) {
1234            throw new IllegalStateException("does not represent array");
1235        }
1236        asArrayNode().add(value);
1237        return this;
1238    }
1239
1240    public Iterable<JsonRepresentation> arrayIterable() {
1241        return arrayIterable(JsonRepresentation.class);
1242    }
1243
1244    public <T> Iterable<T> arrayIterable(final Class<T> requiredType) {
1245        return new Iterable<T>() {
1246            @Override
1247            public Iterator<T> iterator() {
1248                return arrayIterator(requiredType);
1249            }
1250        };
1251    }
1252
1253    public Iterator<JsonRepresentation> arrayIterator() {
1254        return arrayIterator(JsonRepresentation.class);
1255    }
1256
1257    public <T> Iterator<T> arrayIterator(final Class<T> requiredType) {
1258        ensureIsAnArrayAtLeastAsLargeAs(0);
1259        final Function<JsonNode, ?> transformer = representationInstantiatorFor(requiredType);
1260        final ArrayNode arrayNode = (ArrayNode) jsonNode;
1261        final Iterator<JsonNode> iterator = arrayNode.iterator();
1262        // necessary to do in two steps
1263        final Function<JsonNode, T> typedTransformer = asT(transformer); 
1264        return Iterators.transform(iterator, typedTransformer);
1265    }
1266
1267    @SuppressWarnings("unchecked")
1268    private static <T> Function<JsonNode, T> asT(final Function<JsonNode, ?> transformer) {
1269        return (Function<JsonNode, T>) transformer;
1270    }
1271
1272    public JsonRepresentation arrayGet(final int i) {
1273        ensureIsAnArrayAtLeastAsLargeAs(i+1);
1274        return new JsonRepresentation(jsonNode.get(i));
1275    }
1276
1277    public JsonRepresentation arraySetElementAt(final int i, final JsonRepresentation objectRepr) {
1278        ensureIsAnArrayAtLeastAsLargeAs(i+1);
1279        if (objectRepr.isArray()) {
1280            throw new IllegalArgumentException("Representation being set cannot be an array");
1281        }
1282        // can safely downcast because *this* representation is an array
1283        final ArrayNode arrayNode = (ArrayNode) jsonNode;
1284        arrayNode.set(i, objectRepr.asJsonNode());
1285        return this;
1286    }
1287
1288    private void ensureIsAnArrayAtLeastAsLargeAs(final int i) {
1289        if (!jsonNode.isArray()) {
1290            throw new IllegalStateException("Is not an array");
1291        }
1292        if (i > size()) {
1293            throw new IndexOutOfBoundsException("array has only " + size() + " elements");
1294        }
1295    }
1296
1297    // ///////////////////////////////////////////////////////////////////////
1298    // mutable (map)
1299    // ///////////////////////////////////////////////////////////////////////
1300
1301    public boolean mapHas(final String key) {
1302        if (!isMap()) {
1303            throw new IllegalStateException("does not represent map");
1304        }
1305        ObjectNode node = asObjectNode();
1306
1307        final String[] paths = key.split("\\.");
1308        for (int i = 0; i < paths.length; i++) {
1309            final String path = paths[i];
1310            final boolean has = node.has(path);
1311            if (!has) {
1312                return false;
1313            }
1314            if (i + 1 < paths.length) {
1315                // not on last
1316                final JsonNode subNode = node.get(path);
1317                if (!subNode.isObject()) {
1318                    return false;
1319                }
1320                node = (ObjectNode) subNode;
1321            }
1322        }
1323        return true;
1324    }
1325
1326    public JsonRepresentation mapPut(final String key, final List<Object> value) {
1327        if (!isMap()) {
1328            throw new IllegalStateException("does not represent map");
1329        }
1330        if (value == null) {
1331            return this;
1332        }
1333        final JsonRepresentation array = JsonRepresentation.newArray();
1334        for (final Object v : value) {
1335            array.arrayAdd(v);
1336        }
1337        mapPut(key, array);
1338        return this;
1339    }
1340
1341    public JsonRepresentation mapPut(final String key, final Object value) {
1342        if (!isMap()) {
1343            throw new IllegalStateException("does not represent map");
1344        }
1345        final Path path = Path.parse(key);
1346        final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead());
1347        node.put(path.getTail(), value != null ? new POJONode(value) : NullNode.getInstance());
1348        return this;
1349    }
1350
1351    public JsonRepresentation mapPut(final String key, final JsonRepresentation value) {
1352        if (!isMap()) {
1353            throw new IllegalStateException("does not represent map");
1354        }
1355        if (value == null) {
1356            return this;
1357        }
1358        final Path path = Path.parse(key);
1359        final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead());
1360        node.put(path.getTail(), value.asJsonNode());
1361        return this;
1362    }
1363
1364    public JsonRepresentation mapPut(final String key, final String value) {
1365        if (!isMap()) {
1366            throw new IllegalStateException("does not represent map");
1367        }
1368        if (value == null) {
1369            return this;
1370        }
1371        final Path path = Path.parse(key);
1372        final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead());
1373        node.put(path.getTail(), value);
1374        return this;
1375    }
1376
1377    public JsonRepresentation mapPut(final String key, final JsonNode value) {
1378        if (!isMap()) {
1379            throw new IllegalStateException("does not represent map");
1380        }
1381        if (value == null) {
1382            return this;
1383        }
1384        final Path path = Path.parse(key);
1385        final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead());
1386        node.put(path.getTail(), value);
1387        return this;
1388    }
1389
1390    public JsonRepresentation mapPut(final String key, final byte value) {
1391        return mapPut(key, (int)value);
1392    }
1393
1394    public JsonRepresentation mapPut(final String key, final Byte value) {
1395        return value != null ? mapPut(key, value.byteValue()) : mapPut(key, (Object) value);
1396    }
1397
1398    public JsonRepresentation mapPut(final String key, final short value) {
1399        return mapPut(key, (int)value);
1400    }
1401
1402    public JsonRepresentation mapPut(final String key, final Short value) {
1403        return value != null ? mapPut(key, value.shortValue()) : mapPut(key, (Object) value);
1404    }
1405
1406    public JsonRepresentation mapPut(final String key, final int value) {
1407        if (!isMap()) {
1408            throw new IllegalStateException("does not represent map");
1409        }
1410        final Path path = Path.parse(key);
1411        final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead());
1412        node.put(path.getTail(), value);
1413        return this;
1414    }
1415
1416    public JsonRepresentation mapPut(final String key, final Integer value) {
1417        return value != null ? mapPut(key, value.intValue()) : mapPut(key, (Object) value);
1418    }
1419
1420    public JsonRepresentation mapPut(final String key, final long value) {
1421        if (!isMap()) {
1422            throw new IllegalStateException("does not represent map");
1423        }
1424        final Path path = Path.parse(key);
1425        final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead());
1426        node.put(path.getTail(), value);
1427        return this;
1428    }
1429
1430    public JsonRepresentation mapPut(final String key, final Long value) {
1431        return value != null ? mapPut(key, value.longValue()) : mapPut(key, (Object) value);
1432    }
1433
1434    public JsonRepresentation mapPut(final String key, final float value) {
1435        if (!isMap()) {
1436            throw new IllegalStateException("does not represent map");
1437        }
1438        final Path path = Path.parse(key);
1439        final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead());
1440        node.put(path.getTail(), value);
1441        return this;
1442    }
1443
1444    public JsonRepresentation mapPut(final String key, final Float value) {
1445        return value != null ? mapPut(key, value.floatValue()) : mapPut(key, (Object) value);
1446    }
1447
1448    public JsonRepresentation mapPut(final String key, final double value) {
1449        if (!isMap()) {
1450            throw new IllegalStateException("does not represent map");
1451        }
1452        final Path path = Path.parse(key);
1453        final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead());
1454        node.put(path.getTail(), value);
1455        return this;
1456    }
1457
1458    public JsonRepresentation mapPut(final String key, final Double value) {
1459        return value != null ? mapPut(key, value.doubleValue()) : mapPut(key, (Object) value);
1460    }
1461
1462    public JsonRepresentation mapPut(final String key, final boolean value) {
1463        if (!isMap()) {
1464            throw new IllegalStateException("does not represent map");
1465        }
1466        final Path path = Path.parse(key);
1467        final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead());
1468        node.put(path.getTail(), value);
1469        return this;
1470    }
1471
1472    public JsonRepresentation mapPut(final String key, final Boolean value) {
1473        return value != null ? mapPut(key, value.booleanValue()) : mapPut(key, (Object) value);
1474    }
1475
1476    public JsonRepresentation mapPut(final String key, final char value) {
1477        return mapPut(key, ""+value);
1478    }
1479
1480    public JsonRepresentation mapPut(final String key, final Character value) {
1481        return value != null ? mapPut(key, value.charValue()) : mapPut(key, (Object) value);
1482    }
1483
1484    public JsonRepresentation mapPut(final String key, final BigInteger value) {
1485        if (!isMap()) {
1486            throw new IllegalStateException("does not represent map");
1487        }
1488        final Path path = Path.parse(key);
1489        final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead());
1490        if (value != null) {
1491            node.put(path.getTail(), value.toString());
1492        } else {
1493            node.put(path.getTail(), NullNode.getInstance());
1494        }
1495        return this;
1496    }
1497
1498    public JsonRepresentation mapPut(Iterable<Entry<String, JsonRepresentation>> entries) {
1499        for (Entry<String, JsonRepresentation> entry : entries) {
1500            mapPut(entry);
1501        }
1502        return this;
1503    }
1504
1505    public JsonRepresentation mapPut(Entry<String, JsonRepresentation> entry) {
1506        mapPut(entry.getKey(), entry.getValue());
1507        return this;
1508    }
1509
1510    public JsonRepresentation mapPut(final String key, final BigDecimal value) {
1511        if (!isMap()) {
1512            throw new IllegalStateException("does not represent map");
1513        }
1514        final Path path = Path.parse(key);
1515        final ObjectNode node = JsonNodeUtils.walkNodeUpTo(asObjectNode(), path.getHead());
1516        if (value != null) {
1517            node.put(path.getTail(), value.toString());
1518        } else {
1519            node.put(path.getTail(), NullNode.getInstance());
1520        }
1521        return this;
1522    }
1523
1524    private static class Path {
1525        private final List<String> head;
1526        private final String tail;
1527
1528        private Path(final List<String> head, final String tail) {
1529            this.head = Collections.unmodifiableList(head);
1530            this.tail = tail;
1531        }
1532
1533        public List<String> getHead() {
1534            return head;
1535        }
1536
1537        public String getTail() {
1538            return tail;
1539        }
1540
1541        public static Path parse(final String pathStr) {
1542            final List<String> keyList = Lists.newArrayList(Arrays.asList(pathStr.split("\\.")));
1543            if (keyList.size() == 0) {
1544                throw new IllegalArgumentException(String.format("Malformed path '%s'", pathStr));
1545            }
1546            final String tail = keyList.remove(keyList.size() - 1);
1547            return new Path(keyList, tail);
1548        }
1549    }
1550
1551    public Iterable<Map.Entry<String, JsonRepresentation>> mapIterable() {
1552        ensureIsAMap();
1553        return new Iterable<Map.Entry<String, JsonRepresentation>>() {
1554
1555            @Override
1556            public Iterator<Entry<String, JsonRepresentation>> iterator() {
1557                return mapIterator();
1558            }
1559        };
1560    }
1561
1562    public Iterator<Map.Entry<String, JsonRepresentation>> mapIterator() {
1563        ensureIsAMap();
1564        return Iterators.transform(jsonNode.getFields(), MAP_ENTRY_JSON_NODE_TO_JSON_REPRESENTATION);
1565    }
1566
1567    private void ensureIsAMap() {
1568        if (!jsonNode.isObject()) {
1569            throw new IllegalStateException("Is not a map");
1570        }
1571    }
1572
1573    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>>() {
1574
1575        @Override
1576        public Entry<String, JsonRepresentation> apply(final Entry<String, JsonNode> input) {
1577            return new Map.Entry<String, JsonRepresentation>() {
1578
1579                @Override
1580                public String getKey() {
1581                    return input.getKey();
1582                }
1583
1584                @Override
1585                public JsonRepresentation getValue() {
1586                    return new JsonRepresentation(input.getValue());
1587                }
1588
1589                @Override
1590                public JsonRepresentation setValue(final JsonRepresentation value) {
1591                    final JsonNode setValue = input.setValue(value.asJsonNode());
1592                    return new JsonRepresentation(setValue);
1593                }
1594            };
1595        }
1596    };
1597
1598    // ///////////////////////////////////////////////////////////////////////
1599    // helpers
1600    // ///////////////////////////////////////////////////////////////////////
1601
1602    /**
1603     * A reciprocal of the behaviour of the automatic dereferencing of arrays
1604     * that occurs when there is only a single instance.
1605     * 
1606     * @see #toJsonNode(List)
1607     */
1608    public JsonRepresentation ensureArray() {
1609        if (jsonNode.isArray()) {
1610            return this;
1611        }
1612        final JsonRepresentation arrayRepr = JsonRepresentation.newArray();
1613        arrayRepr.arrayAdd(jsonNode);
1614        return arrayRepr;
1615    }
1616
1617    private JsonNode getNode(final String path) {
1618        return getNodeAndFormat(path).node;
1619    }
1620
1621    private static class NodeAndFormat {
1622        JsonNode node;
1623        String format;
1624        NodeAndFormat(JsonNode jsonNode, String format) {
1625            node = jsonNode;
1626            this.format = format;
1627        }
1628    }
1629
1630    /**
1631     * Walks the path to the specified node, and also returns the value of 'format' in the parent node if present.
1632     */
1633    private NodeAndFormat getNodeAndFormat(final String path) {
1634        JsonNode jsonNode = this.jsonNode;
1635        final List<String> keys = PathNode.split(path);
1636        String format = null;
1637        for (final String key : keys) {
1638            final PathNode pathNode = PathNode.parse(key);
1639            if (!pathNode.getKey().isEmpty()) {
1640                // grab format (if present) before moving down the path
1641                format = getFormatValueIfAnyFrom(jsonNode);
1642                jsonNode = jsonNode.path(pathNode.getKey());
1643            } else {
1644                // pathNode is criteria only; don't change jsonNode
1645            }
1646            if (jsonNode.isNull()) {
1647                return new NodeAndFormat(jsonNode, format);
1648            }
1649            if (!pathNode.hasCriteria()) {
1650                continue;
1651            }
1652            if (!jsonNode.isArray()) {
1653                return new NodeAndFormat(NullNode.getInstance(), format);
1654            }
1655            // grab format (if present) before moving down the path
1656            format = getFormatValueIfAnyFrom(jsonNode);
1657            jsonNode = matching(jsonNode, pathNode);
1658            if (jsonNode.isNull()) {
1659                return new NodeAndFormat(jsonNode, format);
1660            }
1661        }
1662        return new NodeAndFormat(jsonNode, format);
1663    }
1664
1665    private String getFormatValueIfAnyFrom(JsonNode jsonNode) {
1666        String format;
1667        final JsonNode formatNode = jsonNode.get("format");
1668        format = formatNode != null && formatNode.isTextual()? formatNode.getTextValue(): null;
1669        return format;
1670    }
1671
1672    private JsonNode matching(final JsonNode jsonNode, final PathNode pathNode) {
1673        final JsonRepresentation asList = new JsonRepresentation(jsonNode);
1674        final Iterable<JsonNode> filtered = Iterables.filter(asList.arrayIterable(JsonNode.class), new Predicate<JsonNode>() {
1675            @Override
1676            public boolean apply(final JsonNode input) {
1677                return pathNode.matches(new JsonRepresentation(input));
1678            }
1679        });
1680        final List<JsonNode> matching = Lists.newArrayList(filtered);
1681        return toJsonNode(matching);
1682    }
1683
1684    private static JsonNode toJsonNode(final List<JsonNode> matching) {
1685        switch (matching.size()) {
1686        case 0:
1687            return NullNode.getInstance();
1688        case 1:
1689            return matching.get(0);
1690        default:
1691            final ArrayNode arrayNode = new ArrayNode(JsonNodeFactory.instance);
1692            arrayNode.addAll(matching);
1693            return arrayNode;
1694        }
1695    }
1696
1697    private static void checkValue(final String path, final JsonNode node, final String requiredType) {
1698        if (node.isValueNode()) {
1699            return;
1700        }
1701        throw new IllegalArgumentException(formatExMsg(path, "is not " + requiredType));
1702    }
1703
1704    private static boolean representsNull(final JsonNode node) {
1705        return node == null || node.isMissingNode() || node.isNull();
1706    }
1707
1708    private static String formatExMsg(final String pathIfAny, final String errorText) {
1709        final StringBuilder buf = new StringBuilder();
1710        if (pathIfAny != null) {
1711            buf.append("'").append(pathIfAny).append("' ");
1712        }
1713        buf.append(errorText);
1714        return buf.toString();
1715    }
1716
1717
1718    // ///////////////////////////////////////////////////////////////////////
1719    // equals and hashcode
1720    // ///////////////////////////////////////////////////////////////////////
1721
1722    @Override
1723    public int hashCode() {
1724        final int prime = 31;
1725        int result = 1;
1726        result = prime * result + ((jsonNode == null) ? 0 : jsonNode.hashCode());
1727        return result;
1728    }
1729
1730    @Override
1731    public boolean equals(Object obj) {
1732        if (this == obj)
1733            return true;
1734        if (obj == null)
1735            return false;
1736        if (getClass() != obj.getClass())
1737            return false;
1738        JsonRepresentation other = (JsonRepresentation) obj;
1739        if (jsonNode == null) {
1740            if (other.jsonNode != null)
1741                return false;
1742        } else if (!jsonNode.equals(other.jsonNode))
1743            return false;
1744        return true;
1745    }
1746    
1747    // ///////////////////////////////////////////////////////////////////////
1748    // toString
1749    // ///////////////////////////////////////////////////////////////////////
1750
1751    @Override
1752    public String toString() {
1753        return jsonNode.toString();
1754    }
1755
1756
1757
1758}
1759