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