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