001/*
002 * Copyright 2012 Mark Slater
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
005 *
006 *      http://www.apache.org/licenses/LICENSE-2.0
007 *
008 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
009 */
010
011package argo.jdom;
012
013import java.util.List;
014import java.util.Map;
015
016import static argo.jdom.JsonNodeDoesNotMatchPathElementsException.jsonNodeDoesNotMatchPathElementsException;
017
018/**
019 * <p>A node (leaf or otherwise) in a JSON document.</p>
020 * <p/>
021 * <p>Supplies methods for examining the node, and also examining and navigating the hierarchy at and below this node.
022 * Methods for navigating the hierarchy are of the form {@code getXXXValue(Object... pathElements)}.</p>
023 * <p/>
024 * <p>For example, {@link #getStringValue(Object...)} takes a series of {@code String}s and
025 * {@code Integer}s as its argument which tell it how to navigate down a hierarchy to a particular JSON string.
026 * The {@code String}s tell it to select a field with the given name from an object, and the {@code Integer}s
027 * tell it to select an element with the given index from an array.</p> If no field of that name exists, or the field
028 * exists, but it isn't a JSON string, an {@code IllegalArgumentException} is thrown.</p>
029 * <p/>
030 * <p>Methods for examining the hierarchy work on the same principal as the
031 * {@code getXXXValue(Object... pathElements)} methods, but return a {@code boolean} indicating whether
032 * or not the element at the given path exists and is of the type specified, for example,
033 * {@code getStringValue("my field")} returns {@code true} if the node has a field called
034 * "{@code my field}", and its value is a JSON string.</p>
035 */
036public abstract class JsonNode {
037
038    // Only extensible by classes in this package
039    JsonNode() {
040    }
041
042    public abstract JsonNodeType getType();
043
044    public abstract boolean hasText();
045
046    /**
047     * @return the text associated with this node
048     * @throws IllegalStateException if hasText() returns false, indicating this type of node doesn't have text.
049     */
050    public abstract String getText();
051
052    public abstract boolean hasFields();
053
054    /**
055     * Gets the fields associated with this node as a map of name to value.  Note that JSON permits
056     * duplicated keys in an object, though in practice this is rare, and in this case, this method
057     * will return a map containing a single entry of each unique key.
058     *
059     * @return the fields associated with this node
060     * @throws IllegalStateException if hasFields() returns false, indicating this type of node doesn't support fields.
061     */
062    public abstract Map<JsonStringNode, JsonNode> getFields();
063
064    /**
065     * Gets the fields associated with this node as a list of {@code JsonFields}.  This method allows
066     * the retrieval of all fields in an object even when the fields have duplicate keys.  This method
067     * also preserves the order of the fields.
068     *
069     * @return the fields associated with this node
070     * @throws IllegalStateException if hasFields() returns false, indicating this type of node doesn't support fields.
071     */
072    public abstract List<JsonField> getFieldList();
073
074    public abstract boolean hasElements();
075
076    /**
077     * @return the elements associated with this node
078     * @throws IllegalStateException if hasElements() returns false, indicating this type of node doesn't support elements.
079     */
080    public abstract List<JsonNode> getElements();
081
082    /**
083     * Determines whether the node at the given path exists.
084     *
085     * @param pathElements a series of {@code String}s, representing the names of fields on objects, and {@code Integer}s, representing elements of arrays indicating how to navigate from this node.
086     * @return whether a JSON node exists at the path given.
087     */
088    public boolean isNode(final Object... pathElements) {
089        return JsonNodeSelectors.anyNode(pathElements).matches(this);
090    }
091
092    /**
093     * Gets a {@code JsonNode} by navigating the hierarchy below this node.
094     *
095     * @param pathElements a series of {@code String}s, representing the names of fields on objects, and {@code Integer}s, representing elements of arrays indicating how to navigate from this node.
096     * @return the {@code JsonNode} at the path given.
097     * @throws IllegalArgumentException if there is no node at the given path.
098     */
099    public JsonNode getNode(final Object... pathElements) {
100        return wrapExceptionsFor(JsonNodeSelectors.anyNode(pathElements), this, pathElements);
101    }
102
103    /**
104     * Determines whether the node at the given path exists and is a root type - either a JSON object or JSON array.
105     *
106     * @param pathElements a series of {@code String}s, representing the names of fields on objects, and {@code Integer}s, representing elements of arrays indicating how to navigate from this node.
107     * @return whether a JSON root node exists at the path given.
108     */
109    public boolean isRootNode(final Object... pathElements) {
110        return JsonNodeSelectors.anyRootNode(pathElements).matches(this);
111    }
112
113    /**
114     * Gets a {@code JsonRootNode} by navigating the hierarchy below this node.
115     *
116     * @param pathElements a series of {@code String}s, representing the names of fields on objects, and {@code Integer}s, representing elements of arrays indicating how to navigate from this node.
117     * @return the {@code JsonRootNode} at the path given.
118     * @throws IllegalArgumentException if there is no node at the given path, or the node at the given path is not a JSON root node.
119     */
120    public JsonRootNode getRootNode(final Object... pathElements) {
121        return wrapExceptionsFor(JsonNodeSelectors.anyRootNode(pathElements), this, pathElements);
122    }
123
124    /**
125     * Determines whether the node at the given path exists and is a JSON boolean.
126     *
127     * @param pathElements a series of {@code String}s, representing the names of fields on objects, and {@code Integer}s, representing elements of arrays indicating how to navigate from this node.
128     * @return whether a JSON boolean exists at the path given.
129     */
130    public final boolean isBooleanValue(final Object... pathElements) {
131        return JsonNodeSelectors.aBooleanNode(pathElements).matches(this);
132    }
133
134    /**
135     * Gets a {@code Boolean} by navigating the hierarchy below this node.
136     *
137     * @param pathElements a series of {@code String}s, representing the names of fields on objects, and {@code Integer}s, representing elements of arrays indicating how to navigate from this node.
138     * @return the {@code Boolean} at the path given.
139     * @throws IllegalArgumentException if there is no node at the given path, or the node at the given path is not a JSON boolean.
140     */
141    public final Boolean getBooleanValue(final Object... pathElements) {
142        return wrapExceptionsFor(JsonNodeSelectors.aBooleanNode(pathElements), this, pathElements);
143    }
144
145    /**
146     * Determines whether the node at the given path exists and is a JSON boolean or a JSON null.
147     *
148     * @param pathElements a series of {@code String}s, representing the names of fields on objects, and {@code Integer}s, representing elements of arrays indicating how to navigate from this node.
149     * @return whether a JSON boolean or a JSON null exists at the path given.
150     */
151    public final boolean isNullableBooleanValue(final Object... pathElements) {
152        return JsonNodeSelectors.aNullableBooleanNode(pathElements).matches(this);
153    }
154
155    /**
156     * Gets a {@code Boolean} or {@code null} by navigating the hierarchy below this node.
157     *
158     * @param pathElements a series of {@code String}s, representing the names of fields on objects, and {@code Integer}s, representing elements of arrays indicating how to navigate from this node.
159     * @return the {@code Boolean} at the path given, or null, if there is a JSON null at the path given.
160     * @throws IllegalArgumentException if there is no node at the given path, or the node at the given path is not a JSON boolean or a JSON null.
161     */
162    public final Boolean getNullableBooleanValue(final Object... pathElements) {
163        return wrapExceptionsFor(JsonNodeSelectors.aNullableBooleanNode(pathElements), this, pathElements);
164    }
165
166    /**
167     * Determines whether the node at the given path exists and is a JSON string.
168     *
169     * @param pathElements a series of {@code String}s, representing the names of fields on objects, and {@code Integer}s, representing elements of arrays indicating how to navigate from this node.
170     * @return whether a JSON string exists at the path given.
171     */
172    public final boolean isStringValue(final Object... pathElements) {
173        return JsonNodeSelectors.aStringNode(pathElements).matches(this);
174    }
175
176    /**
177     * Gets a {@code String} by navigating the hierarchy below this node.
178     *
179     * @param pathElements a series of {@code String}s, representing the names of fields on objects, and {@code Integer}s, representing elements of arrays indicating how to navigate from this node.
180     * @return the {@code String} at the path given
181     * @throws IllegalArgumentException if there is no node at the given path, or the node at the given path is not a JSON string.
182     */
183    public final String getStringValue(final Object... pathElements) {
184        return wrapExceptionsFor(JsonNodeSelectors.aStringNode(pathElements), this, pathElements);
185    }
186
187    /**
188     * Determines whether the node at the given path exists and is a JSON string or a JSON null.
189     *
190     * @param pathElements a series of {@code String}s, representing the names of fields on objects, and {@code Integer}s, representing elements of arrays indicating how to navigate from this node.
191     * @return whether a JSON string or a JSON null exists at the path given.
192     */
193    public final boolean isNullableStringValue(final Object... pathElements) {
194        return JsonNodeSelectors.aNullableStringNode(pathElements).matches(this);
195    }
196
197    /**
198     * Gets a {@code String} or {@code null} by navigating the hierarchy below this node.
199     *
200     * @param pathElements a series of {@code String}s, representing the names of fields on objects, and {@code Integer}s, representing elements of arrays indicating how to navigate from this node.
201     * @return the {@code String} at the path given, or null, if there is a JSON null at the path given.
202     * @throws IllegalArgumentException if there is no node at the given path, or the node at the given path is not a JSON string or a JSON null.
203     */
204    public final String getNullableStringValue(final Object... pathElements) {
205        return wrapExceptionsFor(JsonNodeSelectors.aNullableStringNode(pathElements), this, pathElements);
206    }
207
208    /**
209     * Determines whether the node at the given path exists and is a JSON number.
210     *
211     * @param pathElements a series of {@code String}s, representing the names of fields on objects, and {@code Integer}s, representing elements of arrays indicating how to navigate from this node.
212     * @return whether a JSON number exists at the path given.
213     */
214    public final boolean isNumberValue(final Object... pathElements) {
215        return JsonNodeSelectors.aNumberNode(pathElements).matches(this);
216    }
217
218    /**
219     * Gets a numeric {@code String} by navigating the hierarchy below this node.
220     *
221     * @param pathElements a series of {@code String}s, representing the names of fields on objects, and {@code Integer}s, representing elements of arrays indicating how to navigate from this node.
222     * @return the numeric {@code String} at the path given.
223     * @throws IllegalArgumentException if there is no node at the given path, or the node at the given path is not a JSON number.
224     */
225    public final String getNumberValue(final Object... pathElements) {
226        return wrapExceptionsFor(JsonNodeSelectors.aNumberNode(pathElements), this, pathElements);
227    }
228
229    /**
230     * Determines whether the node at the given path exists and is a JSON number or a JSON null.
231     *
232     * @param pathElements a series of {@code String}s, representing the names of fields on objects, and {@code Integer}s, representing elements of arrays indicating how to navigate from this node.
233     * @return whether a JSON number or a JSON null exists at the path given.
234     */
235    public final boolean isNullableNumberNode(final Object... pathElements) {
236        return JsonNodeSelectors.aNullableNumberNode(pathElements).matches(this);
237    }
238
239    /**
240     * Gets a numeric {@code String} or {@code null} by navigating the hierarchy below this node.
241     *
242     * @param pathElements a series of {@code String}s, representing the names of fields on objects, and {@code Integer}s, representing elements of arrays indicating how to navigate from this node.
243     * @return the numeric {@code String} at the path given, or null, if there is a JSON null at the path given.
244     * @throws IllegalArgumentException if there is no node at the given path, or the node at the given path is not a JSON number or a JSON null.
245     */
246    public final String getNullableNumberValue(final Object... pathElements) {
247        return wrapExceptionsFor(JsonNodeSelectors.aNullableNumberNode(pathElements), this, pathElements);
248    }
249
250    /**
251     * Determines whether the node at the given path exists and is a JSON null.
252     *
253     * @param pathElements a series of {@code String}s, representing the names of fields on objects, and {@code Integer}s, representing elements of arrays indicating how to navigate from this node.
254     * @return whether a JSON null exists at the path given.
255     */
256    public final boolean isNullNode(final Object... pathElements) {
257        return JsonNodeSelectors.aNullNode(pathElements).matches(this);
258    }
259
260    /**
261     * Gets a {@code JsonNode} representing null by navigating the hierarchy below this node.
262     *
263     * @param pathElements a series of {@code String}s, representing the names of fields on objects, and {@code Integer}s, representing elements of arrays indicating how to navigate from this node.
264     * @return a {@code JsonNode} representing null.
265     * @throws IllegalArgumentException if there is no node at the given path, or the node at the given path is not a JSON null.
266     */
267    public final JsonNode getNullNode(final Object... pathElements) {
268        return wrapExceptionsFor(JsonNodeSelectors.aNullNode(pathElements), this, pathElements);
269    }
270
271    /**
272     * Determines whether the node at the given path exists and is a JSON object.
273     *
274     * @param pathElements a series of {@code String}s, representing the names of fields on objects, and {@code Integer}s, representing elements of arrays indicating how to navigate from this node.
275     * @return whether a JSON object exists at the path given.
276     */
277    public final boolean isObjectNode(final Object... pathElements) {
278        return JsonNodeSelectors.anObjectNode(pathElements).matches(this);
279    }
280
281    /**
282     * Gets a {@code Map} of {@code String} field names to {@code JsonNode}s, representing a JSON object, by navigating the hierarchy below this node.
283     *
284     * @param pathElements a series of {@code String}s, representing the names of fields on objects, and {@code Integer}s, representing elements of arrays indicating how to navigate from this node.
285     * @return a {@code Map} of {@code String} field names to {@code JsonNode}s representing a JSON object.
286     * @throws IllegalArgumentException if there is no node at the given path, or the node at the given path is not a JSON object.
287     */
288    public final Map<JsonStringNode, JsonNode> getObjectNode(final Object... pathElements) {
289        return wrapExceptionsFor(JsonNodeSelectors.anObjectNode(pathElements), this, pathElements);
290    }
291
292    /**
293     * Determines whether the node at the given path exists and is a JSON object or a JSON null.
294     *
295     * @param pathElements a series of {@code String}s, representing the names of fields on objects, and {@code Integer}s, representing elements of arrays indicating how to navigate from this node.
296     * @return whether a JSON object or a JSON null exists at the path given.
297     */
298    public final boolean isNullableObjectNode(final Object... pathElements) {
299        return JsonNodeSelectors.aNullableObjectNode(pathElements).matches(this);
300    }
301
302    /**
303     * Gets a {@code Map} of {@code String} field names to {@code JsonNode}s, representing a JSON object, or {@code null} by navigating the hierarchy below this node.
304     *
305     * @param pathElements a series of {@code String}s, representing the names of fields on objects, and {@code Integer}s, representing elements of arrays indicating how to navigate from this node.
306     * @return a {@code Map} of {@code String} field names to {@code JsonNode}s representing a JSON object at the path given, or null, if there is a JSON null at the path given.
307     * @throws IllegalArgumentException if there is no node at the given path, or the node at the given path is not a JSON object or a JSON null.
308     */
309    public final Map<JsonStringNode, JsonNode> getNullableObjectNode(final Object... pathElements) {
310        return wrapExceptionsFor(JsonNodeSelectors.aNullableObjectNode(pathElements), this, pathElements);
311    }
312
313    /**
314     * Determines whether the node at the given path exists and is a JSON array.
315     *
316     * @param pathElements a series of {@code String}s, representing the names of fields on objects, and {@code Integer}s, representing elements of arrays indicating how to navigate from this node.
317     * @return whether a JSON array exists at the path given.
318     */
319    public final boolean isArrayNode(final Object... pathElements) {
320        return JsonNodeSelectors.anArrayNode(pathElements).matches(this);
321    }
322
323    /**
324     * Gets a {@code List} of {@code JsonNode}s, representing a JSON array, by navigating the hierarchy below this node.
325     *
326     * @param pathElements a series of {@code String}s, representing the names of fields on objects, and {@code Integer}s, representing elements of arrays indicating how to navigate from this node.
327     * @return a {@code List} of {@code JsonNode}s representing a JSON array.
328     * @throws IllegalArgumentException if there is no node at the given path, or the node at the given path is not a JSON array.
329     */
330    public final List<JsonNode> getArrayNode(final Object... pathElements) {
331        return wrapExceptionsFor(JsonNodeSelectors.anArrayNode(pathElements), this, pathElements);
332    }
333
334    /**
335     * Determines whether the node at the given path exists and is a JSON array or a JSON null.
336     *
337     * @param pathElements a series of {@code String}s, representing the names of fields on objects, and {@code Integer}s, representing elements of arrays indicating how to navigate from this node.
338     * @return whether a JSON array or a JSON null exists at the path given.
339     */
340    public final boolean isNullableArrayNode(final Object... pathElements) {
341        return JsonNodeSelectors.aNullableArrayNode(pathElements).matches(this);
342    }
343
344    /**
345     * Gets a {@code List} of {@code JsonNode}s, representing a JSON array, or {@code null} by navigating the hierarchy below this node.
346     *
347     * @param pathElements a series of {@code String}s, representing the names of fields on objects, and {@code Integer}s, representing elements of arrays indicating how to navigate from this node.
348     * @return a {@code List} of {@code JsonNode}s representing a JSON array, or null, if there is a JSON null at the path given.
349     * @throws IllegalArgumentException if there is no node at the given path, or the node at the given path is not a JSON array or a JSON null.
350     */
351    public final List<JsonNode> getNullableArrayNode(final Object... pathElements) {
352        return wrapExceptionsFor(JsonNodeSelectors.aNullableArrayNode(pathElements), this, pathElements);
353    }
354
355    private <T, V extends JsonNode> T wrapExceptionsFor(final JsonNodeSelector<V, T> value, final V node, final Object[] pathElements) throws JsonNodeDoesNotMatchPathElementsException {
356        try {
357            return value.getValue(node);
358        } catch (JsonNodeDoesNotMatchChainedJsonNodeSelectorException e) {
359            throw jsonNodeDoesNotMatchPathElementsException(e, pathElements, JsonNodeFactories.array(node));
360        }
361    }
362
363}