com.jayway.jsonpath.JsonModel.java Source code

Java tutorial

Introduction

Here is the source code for com.jayway.jsonpath.JsonModel.java

Source

/*
 * Copyright 2011 the original author or authors.
 * 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
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * 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.
 */
package com.jayway.jsonpath;

import com.jayway.jsonpath.internal.ConvertUtils;
import com.jayway.jsonpath.internal.JsonFormatter;
import com.jayway.jsonpath.internal.PathToken;
import com.jayway.jsonpath.internal.IOUtils;
import com.jayway.jsonpath.spi.JsonProvider;
import com.jayway.jsonpath.spi.JsonProviderFactory;
import com.jayway.jsonpath.spi.MappingProviderFactory;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.*;

import static java.util.Arrays.asList;
import static org.apache.commons.lang.Validate.*;

/**
 * A JsonModel holds a parsed JSON document and provides easy read and write operations. In contrast to the
 * static read operations provided by {@link JsonPath} a JsonModel will only parse the document once.
 *
 * @author Kalle Stenflo
 */
public class JsonModel {

    private static final JsonPath JSON_PATH_ROOT = JsonPath.compile("$");
    private Object jsonObject;
    private JsonProvider jsonProvider;

    // --------------------------------------------------------
    //
    // Constructors
    //
    // --------------------------------------------------------

    private JsonModel(String jsonObject, JsonProvider jsonProvider) {
        notNull(jsonObject, "json can not be null");

        this.jsonProvider = jsonProvider;
        this.jsonObject = jsonProvider.parse(jsonObject);
    }

    /**
     * Creates a new JsonModel based on a json document.
     * Note that the jsonObject must either a {@link List} or a {@link Map}
     *
     * @param jsonObject   the json object
     * @param jsonProvider
     */
    private JsonModel(Object jsonObject, JsonProvider jsonProvider) {
        notNull(jsonObject, "json can not be null");

        if (!jsonProvider.isContainer(jsonObject)) {
            throw new IllegalArgumentException("Invalid container object");
        }
        this.jsonProvider = jsonProvider;
        this.jsonObject = jsonObject;
    }

    /**
     * Creates a new JsonModel based on an {@link InputStream}
     *
     * @param jsonInputStream the input stream
     * @param jsonProvider
     */
    private JsonModel(InputStream jsonInputStream, JsonProvider jsonProvider) {
        notNull(jsonInputStream, "jsonInputStream can not be null");
        this.jsonProvider = jsonProvider;
        this.jsonObject = jsonProvider.parse(jsonInputStream);
    }

    /**
     * Creates a new JsonModel by fetching the content from the provided URL
     *
     * @param jsonURL      the URL to read
     * @param jsonProvider
     * @throws IOException failed to load URL
     */
    private JsonModel(URL jsonURL, JsonProvider jsonProvider) throws IOException {
        notNull(jsonURL, "jsonURL can not be null");

        InputStream jsonInputStream = null;
        try {
            jsonInputStream = jsonURL.openStream();
            this.jsonObject = jsonProvider.parse(jsonInputStream);
            this.jsonProvider = jsonProvider;
        } finally {
            IOUtils.closeQuietly(jsonInputStream);
        }
    }

    /**
     * Check if this JsonModel is holding a JSON array as to object
     *
     * @return true if root is an array
     */
    public boolean isList() {
        return jsonProvider.isList(jsonObject);
    }

    /**
     * Check if this JsonModel is holding a JSON object as to object
     *
     * @return true if root is an object
     */
    public boolean isMap() {
        return jsonProvider.isMap(jsonObject);
    }

    /**
     * Prints this JsonModel to standard out
     */
    public void print() {
        String json = toJson();
        System.out.println(JsonFormatter.prettyPrint(json));
    }

    /**
     * Check if this JsonModel has the given definite path
     *
     * @see com.jayway.jsonpath.JsonPath#isPathDefinite()
     *
     * @param jsonPath path to check
     * @return true if model contains path
     */
    public boolean hasPath(String jsonPath) {
        return hasPath(JsonPath.compile(jsonPath));
    }

    /**
     * Check if this JsonModel has the given definite path
     *
     * @see com.jayway.jsonpath.JsonPath#isPathDefinite()
     *
     * @param jsonPath path to check
     * @return true if model contains path
     */
    public boolean hasPath(JsonPath jsonPath) {

        isTrue(jsonPath.isPathDefinite(), "hasPath can only be used for definite paths");

        try {
            get(jsonPath);
        } catch (InvalidPathException e) {
            return false;
        }
        return true;
    }

    // --------------------------------------------------------
    //
    // Getters
    //
    // --------------------------------------------------------

    /**
     * Returns the root object of this JsonModel
     *
     * @return returns the root object
     */
    public Object getJsonObject() {
        return this.jsonObject;
    }

    // --------------------------------------------------------
    //
    // Model readers
    //
    // --------------------------------------------------------

    /**
     * Reads the given path from this JsonModel. Filters is a way to problematically filter the contents of a list.
     * Instead of writing the filter criteria directly inside the JsonPath expression the filter is indicated and
     * provided as an argument.
     * <p/>
     * All three statements below are equivalent
     * <p/>
     * <code>
     * JsonModel model = JsonModel.model(myJson);
     * <p/>
     * //A
     * List<String> books = model.read("$store.book[?(@author == 'Nigel Rees')]");
     * <p/>
     * //B
     * List<String> books = model.read("$store.book[?]", filter(where("author").is("Nigel Rees"));
     * <p/>
     * //C
     * JsonPath path = JsonPath.compile("$store.book[?]", filter(where("author").is("Nigel Rees"));
     * <p/>
     * List<String> books = model.read(path);
     * <p/>
     * </code>
     * <p/>
     * The filters are applied in the order they are provided. If a path contains multiple [?] filter markers
     * the filters must be passed in the correct order.
     *
     * @param jsonPath the path to read
     * @param filters  filters to use in the path
     * @param <T>      expected return type
     * @return the json path result
     * @see Filter
     * @see Criteria
     */
    @SuppressWarnings({ "unchecked" })
    public <T> T get(String jsonPath, Filter... filters) {
        return (T) get(JsonPath.compile(jsonPath, filters));
    }

    /**
     * Reads the given path from this JsonModel.
     *
     * @param jsonPath the path to read
     * @param <T>      expected return type
     * @return the json path result
     */
    @SuppressWarnings({ "unchecked" })
    public <T> T get(JsonPath jsonPath) {
        notNull(jsonPath, "jsonPath can not be null");

        return (T) jsonPath.read(jsonObject);
    }

    // --------------------------------------------------------
    //
    // Model writers
    //
    // --------------------------------------------------------

    /**
     * Gets an {@link ArrayOps} for this JsonModel. Note that the root element of this model
     * must be a json array.
     *
     * @return array operations for this JsonModel
     */
    public ArrayOps opsForArray() {
        isTrue(jsonProvider.isList(jsonObject), "This JsonModel is not a JSON array");
        return opsForArray(JSON_PATH_ROOT);
    }

    /**
     * Gets an {@link ArrayOps} for the array inside this JsonModel identified by the given JsonPath. The path must
     * be definite ({@link com.jayway.jsonpath.JsonPath#isPathDefinite()}).
     * <p/>
     * Note that the element returned by the given path must be a json array.
     *
     * @param jsonPath definite path to array to perform operations on
     * @return array operations for the targeted array
     */
    public ArrayOps opsForArray(String jsonPath) {
        return opsForArray(JsonPath.compile(jsonPath));
    }

    /**
     * Gets an {@link ArrayOps} for the array inside this JsonModel identified by the given JsonPath. The path must
     * be definite ({@link com.jayway.jsonpath.JsonPath#isPathDefinite()}).
     * <p/>
     * Note that the element returned by the given path must be a json array.
     *
     * @param jsonPath definite path to array to perform operations on
     * @return array operations for the targeted array
     */
    public ArrayOps opsForArray(JsonPath jsonPath) {
        notNull(jsonPath, "jsonPath can not be null");
        return new DefaultArrayOps(jsonPath);
    }

    /**
     * Gets an {@link ObjectOps} for this JsonModel. Note that the root element of this model
     * must be a json object.
     *
     * @return object operations for this JsonModel
     */
    public ObjectOps opsForObject() {
        return opsForObject(JSON_PATH_ROOT);
    }

    /**
     * Gets an {@link ObjectOps} for the object inside this JsonModel identified by the given JsonPath. The path must
     * be definite ({@link com.jayway.jsonpath.JsonPath#isPathDefinite()}).
     * <p/>
     * Note that the element returned by the given path must be a json object.
     *
     * @param jsonPath definite path to object to perform operations on
     * @return object operations for the targeted object
     */
    public ObjectOps opsForObject(String jsonPath) {
        return opsForObject(JsonPath.compile(jsonPath));
    }

    /**
     * Gets an {@link ObjectOps} for the object inside this JsonModel identified by the given JsonPath. The path must
     * be definite ({@link com.jayway.jsonpath.JsonPath#isPathDefinite()}).
     * <p/>
     * Note that the element returned by the given path must be a json object.
     *
     * @param jsonPath definite path to object to perform operations on
     * @return object operations for the targeted object
     */
    public ObjectOps opsForObject(JsonPath jsonPath) {
        notNull(jsonPath, "jsonPath can not be null");
        return new DefaultObjectOps(jsonPath);
    }

    // --------------------------------------------------------
    //
    // JSON extractors
    //
    // --------------------------------------------------------

    /**
     * Creates a JSON representation of this JsonModel
     *
     * @return model as Json
     */
    public String toJson() {
        return toJson(false);
    }

    /**
     * Creates a JSON representation of this JsonModel
     *
     * @param prettyPrint if the model should be pretty printed
     * @return
     */
    public String toJson(boolean prettyPrint) {
        String json = jsonProvider.toJson(jsonObject);
        if (prettyPrint)
            return JsonFormatter.prettyPrint(json);
        else
            return json;
    }

    /**
     * Creates a JSON representation of the result of the provided JsonPath
     *
     * @return path result as Json
     */
    public String toJson(String jsonPath, Filter... filters) {
        return toJson(JsonPath.compile(jsonPath, filters));
    }

    /**
     * Creates a JSON representation of the result of the provided JsonPath
     *
     * @return path result as Json
     */
    public String toJson(JsonPath jsonPath) {
        notNull(jsonPath, "jsonPath can not be null");

        return jsonProvider.toJson(get(jsonPath));
    }

    // --------------------------------------------------------
    //
    // Sub model readers
    //
    // --------------------------------------------------------

    /**
     * Returns a sub model from this JsonModel. A sub model can be any JSON object or JSON array
     * addressed by a definite path. In contrast to a detached model changes on the sub model
     * will be applied on the source model (the JsonModel from which the sub model was created)
     *
     *
     * @param jsonPath the absolute path to extract a JsonModel for
     * @return the new JsonModel
     *
     * @see com.jayway.jsonpath.JsonPath#isPathDefinite()
     */
    public JsonModel getSubModel(String jsonPath) {
        return getSubModel(JsonPath.compile(jsonPath));
    }

    /**
     * Returns a sub model from this JsonModel. A sub model can be any JSON object or JSON array
     * addressed by a definite path. In contrast to a detached model changes on the sub model
     * will be applied on the source model (the JsonModel from which the sub model was created)
     *
     *
     * @param jsonPath the absolute path to extract a JsonModel for
     * @return the new JsonModel
     *
     * @see com.jayway.jsonpath.JsonPath#isPathDefinite()
     */
    public JsonModel getSubModel(JsonPath jsonPath) {
        notNull(jsonPath, "jsonPath can not be null");

        isTrue(jsonPath.isPathDefinite(),
                "You can only get subModels with a definite path. Use getDetachedModel if path is not definite.");

        Object subModel = jsonPath.read(jsonObject);

        if (!jsonProvider.isContainer(subModel)) {
            throw new InvalidModelException("The path " + jsonPath.getPath() + " returned an invalid model "
                    + (subModel != null ? subModel.getClass() : "null"));
        }

        return new JsonSubModel(subModel, this.jsonProvider, this, jsonPath);
    }
    // --------------------------------------------------------
    //
    // Detached sub model readers
    //
    // --------------------------------------------------------

    /**
     * Creates a detached sub model from this JsonModel. A detached sub model does not have
     * to be created using a definite path. Changes on a detached sub model will not be reflected on the
     * source model (the JsonModel from which the sub model was created).
     *
     * @param jsonPath the absolute path to extract a JsonModel for
     * @param filters filters to expand the path
     * @return a detached JsonModel
     */
    public JsonModel getSubModelDetached(String jsonPath, Filter... filters) {
        return getSubModelDetached(JsonPath.compile(jsonPath, filters));
    }

    /**
     * Creates a detached sub model from this JsonModel. A detached sub model does not have
     * to be created using a definite path. Changes on a detached sub model will not be reflected on the
     * source model (the JsonModel from which the sub model was created).
     *
     * @param jsonPath the absolute path to extract a JsonModel for
     * @return a detached JsonModel
     */
    public JsonModel getSubModelDetached(JsonPath jsonPath) {
        notNull(jsonPath, "jsonPath can not be null");

        Object subModel = jsonPath.read(jsonObject);

        if (!jsonProvider.isContainer(subModel)) {
            throw new InvalidModelException("The path " + jsonPath.getPath() + " returned an invalid model "
                    + (subModel != null ? subModel.getClass() : "null"));
        }

        subModel = jsonProvider.clone(subModel);

        return new JsonModel(subModel, this.jsonProvider);
    }

    // --------------------------------------------------------
    //
    // Mapping model readers
    //
    // --------------------------------------------------------

    /**
     * Returns a {@link MappingModelReader} for this JsonModel. Note that to use this functionality you need
     * an optional dependencies on your classpath (jackson-mapper-asl ver >= 1.9.5)
     *
     * @return a object mapper
     */
    public MappingModelReader map() {
        return new DefaultMappingModelReader(this.jsonObject);
    }

    /**
     * Returns a {@link MappingModelReader} for the JsonModel targeted by the provided {@link JsonPath}. Note that to use this functionality you need
     * an optional dependencies on your classpath (jackson-mapper-asl ver >= 1.9.5)
     *
     * @return a object mapper
     */
    public MappingModelReader map(String jsonPath, Filter... filters) {
        return map(JsonPath.compile(jsonPath, filters));
    }

    /**
     * Returns a {@link MappingModelReader} for the JsonModel targeted by the provided {@link JsonPath}. Note that to use this functionality you need
     * an optional dependencies on your classpath (jackson-mapper-asl ver >= 1.9.5)
     *
     * @return a object mapper
     */
    public MappingModelReader map(JsonPath jsonPath) {
        notNull(jsonPath, "jsonPath can not be null");

        return new DefaultMappingModelReader(JsonModel.this.get(jsonPath));
    }

    // --------------------------------------------------------
    //
    // Static factory methods
    //
    // --------------------------------------------------------

    /**
     * Creates a JsonModel 
     * 
     * @param json json string
     * @return a new JsonModel
     */
    public static JsonModel model(String json) {
        notEmpty(json, "json can not be null or empty");

        return new JsonModel(json, JsonProviderFactory.createProvider());
    }

    public static JsonModel create(String json) {
        return model(json);
    }

    /**
     * Creates a JsonModel 
     * 
     * @param jsonObject a json container (a {@link Map} or a {@link List})
     * @return a new JsonModel
     */
    public static JsonModel model(Object jsonObject) {
        notNull(jsonObject, "jsonObject can not be null");

        return new JsonModel(jsonObject, JsonProviderFactory.createProvider());
    }

    public static JsonModel create(Object jsonObject) {
        return model(jsonObject);
    }

    /**
     * Creates a JsonModel 
     * 
     * @param url pointing to a Json document
     * @return a new JsonModel
     */
    public static JsonModel model(URL url) throws IOException {
        notNull(url, "url can not be null");

        return new JsonModel(url, JsonProviderFactory.createProvider());
    }

    public static JsonModel create(URL url) throws IOException {
        return model(url);
    }

    /**
     * Creates a JsonModel 
     * 
     * @param jsonInputStream json document stream
     * @return a new JsonModel
     */
    public static JsonModel model(InputStream jsonInputStream) throws IOException {
        notNull(jsonInputStream, "jsonInputStream can not be null");

        return new JsonModel(jsonInputStream, JsonProviderFactory.createProvider());
    }

    public static JsonModel create(InputStream jsonInputStream) throws IOException {
        return model(jsonInputStream);
    }

    // --------------------------------------------------------
    //
    // Private helpers
    //
    // --------------------------------------------------------

    private <T> T getTargetObject(JsonPath jsonPath, Class<T> clazz) {
        notNull(jsonPath, "jsonPath can not be null");

        if (!jsonPath.isPathDefinite()) {
            throw new IndefinitePathException(jsonPath.getPath());
        }

        JsonProvider jsonProvider = JsonProviderFactory.createProvider();

        Object modelRef = jsonObject;

        if (jsonPath.getTokenizer().size() == 1) {
            PathToken onlyToken = jsonPath.getTokenizer().iterator().next();
            if ("$".equals(onlyToken.getFragment())) {
                return clazz.cast(modelRef);
            }
        } else {

            LinkedList<PathToken> tokens = jsonPath.getTokenizer().getPathTokens();

            PathToken currentToken;
            do {
                currentToken = tokens.poll();
                modelRef = currentToken.apply(modelRef, jsonProvider);
            } while (!tokens.isEmpty());

            if (modelRef.getClass().isAssignableFrom(clazz)) {
                throw new InvalidModelException(
                        jsonPath + " does nor refer to a Map but " + currentToken.getClass().getName());
            }
            return clazz.cast(modelRef);
        }
        throw new InvalidModelException();
    }

    private void setTargetObject(JsonPath jsonPath, Object newValue) {
        JsonPath setterPath = jsonPath.copy();
        PathToken pathToken = setterPath.getTokenizer().removeLastPathToken();

        if (pathToken.isRootToken()) {
            if (this instanceof JsonSubModel) {
                JsonSubModel thisModel = (JsonSubModel) this;

                thisModel.parent.setTargetObject(thisModel.subModelPath, newValue);
            } else {
                this.jsonObject = newValue;
            }
        } else {
            if (pathToken.isArrayIndexToken()) {
                int arrayIndex = pathToken.getArrayIndex();
                opsForArray(setterPath).set(arrayIndex, newValue);
            } else {
                opsForObject(setterPath).put(pathToken.getFragment(), newValue);
            }
        }
    }

    // --------------------------------------------------------
    //
    // Interfaces
    //
    // --------------------------------------------------------

    /**
     * Converts a {@link JsonModel} to an Object
     */
    public interface ObjectMappingModelReader {
        /**
         * Converts this JsonModel to the specified class using the configured {@link com.jayway.jsonpath.spi.MappingProvider} 
         * 
         * @see MappingProviderFactory
         * 
         * @param targetClass class to convert the {@link JsonModel} to
         * @param <T> template class
         * @return the mapped model
         */
        <T> T to(Class<T> targetClass);
    }

    /**
     * Converts a {@link JsonModel} to an {@link Collection} of Objects
     */
    public interface ListMappingModelReader {
        /**
         * Converts this JsonModel to the a list of objects with the provided class using the configured {@link com.jayway.jsonpath.spi.MappingProvider}
         * 
         * @param targetClass class to convert the {@link JsonModel} array items to
         * @param <T> template class
         * @return the mapped mode
         */
        <T> List<T> of(Class<T> targetClass);

        /**
         * Syntactic sugar function to use with {@link ListMappingModelReader#of}
         */
        ListMappingModelReader toList();

        /**
         * Converts this JsonModel to the a {@link List} of objects with the provided class using the configured {@link com.jayway.jsonpath.spi.MappingProvider}
         * 
         * @param targetClass class to convert the {@link JsonModel} array items to
         * @param <T> template class
         * @return the mapped mode
         */
        <T> List<T> toListOf(Class<T> targetClass);

        /**
         * Converts this JsonModel to the a {@link Set} of objects with the provided class using the configured {@link com.jayway.jsonpath.spi.MappingProvider}
         * 
         * @param targetClass class to convert the {@link JsonModel} array items to
         * @param <T> template class
         * @return the mapped model
         */
        <T> Set<T> toSetOf(Class<T> targetClass);
    }

    /**
     * Object mapping interface used when for root object that can be either a {@link List} or a {@link Map}.
     * It's up to the invoker to know what the conversion target can be mapped to. 
     */
    public interface MappingModelReader extends ListMappingModelReader, ObjectMappingModelReader {
    }

    /**
     * Operations that can be performed on Json objects ({@link Map}s)
     */
    public interface ObjectOps {

        /**
         * Returns the operation target 
         * @return the operation target
         */
        Map<String, Object> getTarget();

        /**
         * @see Map#containsKey(Object)
         */
        boolean containsKey(String key);

        /**
         * @see Map#put(Object, Object)
         */
        ObjectOps put(String key, Object value);

        /**
         * Adds the value to the target map if it is not already present
         * @param key the key
         * @param value the value
         * @return this {@link ObjectOps}
         */
        ObjectOps putIfAbsent(String key, Object value);

        /**
         * @see Map#get(Object)
         */
        Object get(String key);

        /**
         * Tries to convert the value associated with the key to an {@link Integer}
         * @param key the key
         * @return converted value
         */
        Integer getInteger(String key);

        /**
         * Tries to convert the value associated with the key to an {@link Long}
         * @param key the key
         * @return converted value
         */
        Long getLong(String key);

        /**
         * Tries to convert the value associated with the key to an {@link Double}
         * @param key the key
         * @return converted value
         */
        Double getDouble(String key);

        /**
         * Tries to convert the value associated with the key to an {@link String}
         * @param key the key
         * @return converted value
         */
        String getString(String key);

        /**
         * @see Map#putAll(java.util.Map)
         */
        ObjectOps putAll(Map<String, Object> map);

        /**
         * @see Map#remove(Object)
         */
        ObjectOps remove(String key);

        /**
         * Allows transformations of the target object. the target for this {@link ObjectOps} will be be replaced
         * with the {@link Object} returned by the {@link Transformer#transform(Object)}
         *
         * @param transformer the transformer to use
         * @return this {@link ObjectOps}
         */
        ObjectOps transform(Transformer<Map<String, Object>> transformer);

        /**
         * Map the target of this {@link ObjectOps} to the provided class
         * @param targetClass class to convert the target object to
         * @param <T> template class
         * @return the mapped model
         */
        <T> T to(Class<T> targetClass);
    }

    /**
     * Operations that can be performed on Json arrays ({@link List}s)
     */
    public interface ArrayOps {

        /**
         * Returns the operation target
         * @return the operation target
         */
        List<Object> getTarget();

        /**
         * @see List#add(Object)
         */
        ArrayOps add(Object o);

        /**
         * @see List#addAll(java.util.Collection)
         */
        ArrayOps addAll(Collection<Object> collection);

        /**
         * @see List#remove(int)
         */
        ArrayOps remove(Object o);

        /**
         * @see java.util.List#size()
         */
        int size();

        /**
         * @see List#set(int, Object)
         */
        ArrayOps set(int index, Object value);

        /**
         * Allows transformations of the target list. The target for this {@link ArrayOps} will be be replaced
         * with the {@link Object} returned by the {@link Transformer#transform(Object)}
         *
         * @param transformer the transformer to use
         * @return this {@link ArrayOps}
         */
        ArrayOps transform(Transformer<List<Object>> transformer);

        ArrayOps each(Transformer<Object> transformer);

        /**
         * @see ListMappingModelReader
         */
        ListMappingModelReader toList();

        /**
         * @see ListMappingModelReader
         */
        <T> List<T> toListOf(Class<T> targetClass);

        /**
         * @see ListMappingModelReader
         */
        <T> Set<T> toSetOf(Class<T> targetClass);
    }

    private class DefaultObjectOps implements ObjectOps {

        private JsonPath jsonPath;

        private DefaultObjectOps(JsonPath jsonPath) {
            this.jsonPath = jsonPath;
        }

        @Override
        public Map<String, Object> getTarget() {
            return getTargetObject(jsonPath, Map.class);
        }

        @Override
        public boolean containsKey(String key) {
            return getTargetObject(jsonPath, Map.class).containsKey(key);
        }

        @Override
        public ObjectOps put(String key, Object value) {
            getTargetObject(jsonPath, Map.class).put(key, value);
            return this;
        }

        @Override
        public ObjectOps putIfAbsent(String key, Object value) {
            Map targetObject = getTargetObject(jsonPath, Map.class);
            if (!targetObject.containsKey(key)) {
                targetObject.put(key, value);
            }
            return this;
        }

        @Override
        public Object get(String key) {
            return getTargetObject(jsonPath, Map.class).get(key);
        }

        @Override
        public Integer getInteger(String key) {
            return ConvertUtils.toInt(get(key));
        }

        @Override
        public Long getLong(String key) {
            return ConvertUtils.toLong(get(key));
        }

        @Override
        public Double getDouble(String key) {
            return ConvertUtils.toDouble(get(key));
        }

        @Override
        public String getString(String key) {
            return ConvertUtils.toString(get(key));
        }

        @Override
        public ObjectOps putAll(Map<String, Object> map) {
            getTargetObject(jsonPath, Map.class).putAll(map);
            return this;
        }

        @Override
        public ObjectOps remove(String key) {
            getTargetObject(jsonPath, Map.class).remove(key);
            return this;
        }

        @Override
        public ObjectOps transform(Transformer<Map<String, Object>> transformer) {
            Map targetObject = getTargetObject(jsonPath, Map.class);
            Object transformed = transformer.transform(targetObject);
            setTargetObject(jsonPath, transformed);
            return this;
        }

        @Override
        public <T> T to(Class<T> targetClass) {
            Map targetObject = getTargetObject(jsonPath, Map.class);
            return new DefaultMappingModelReader(targetObject).to(targetClass);
        }
    }

    private class DefaultArrayOps implements ArrayOps {

        private JsonPath jsonPath;

        private DefaultArrayOps(JsonPath jsonPath) {
            this.jsonPath = jsonPath;
        }

        @Override
        public List<Object> getTarget() {
            return getTargetObject(jsonPath, List.class);
        }

        @Override
        public ArrayOps add(Object o) {
            getTargetObject(jsonPath, List.class).add(o);
            return this;
        }

        @Override
        public ArrayOps addAll(Collection<Object> collection) {
            getTargetObject(jsonPath, List.class).addAll(collection);
            return this;
        }

        @Override
        public ArrayOps remove(Object o) {
            getTargetObject(jsonPath, List.class).remove(o);
            return this;
        }

        @Override
        public int size() {
            return getTargetObject(jsonPath, List.class).size();
        }

        @Override
        public ArrayOps set(int index, Object value) {
            getTargetObject(jsonPath, List.class).set(index, value);
            return this;
        }

        @Override
        public ListMappingModelReader toList() {
            return new DefaultMappingModelReader(getTargetObject(jsonPath, List.class));
        }

        @Override
        public ArrayOps transform(Transformer<List<Object>> transformer) {
            Object transformed = transformer.transform(getTargetObject(jsonPath, List.class));
            setTargetObject(jsonPath, transformed);
            return this;
        }

        @Override
        public ArrayOps each(Transformer<Object> transformer) {

            List targetObject = getTargetObject(jsonPath, List.class);
            for (int i = 0; i < targetObject.size(); i++) {
                targetObject.set(i, transformer.transform(targetObject.get(i)));
            }
            return this;
        }

        @Override
        public <T> List<T> toListOf(Class<T> targetClass) {
            return new DefaultMappingModelReader(getTargetObject(jsonPath, List.class)).toListOf(targetClass);
        }

        @Override
        public <T> Set<T> toSetOf(Class<T> targetClass) {
            return new DefaultMappingModelReader(getTargetObject(jsonPath, List.class)).toSetOf(targetClass);
        }
    }

    private static class DefaultMappingModelReader implements MappingModelReader {
        private Object model;

        private DefaultMappingModelReader(Object model) {
            this.model = model;
        }

        @Override
        public ListMappingModelReader toList() {
            return this;
        }

        @Override
        public <T> List<T> of(Class<T> targetClass) {
            return toListOf(targetClass);
        }

        @Override
        public <T> List<T> toListOf(Class<T> targetClass) {
            Object modelRef = model;
            if (!(modelRef instanceof List)) {
                modelRef = asList(modelRef);
            }
            return MappingProviderFactory.createProvider().convertValue(modelRef, List.class, targetClass);
        }

        @Override
        public <T> Set<T> toSetOf(Class<T> targetClass) {
            Object modelRef = model;
            if (!(modelRef instanceof List)) {
                Set setModel = new HashSet();
                setModel.add(model);
                modelRef = setModel;
            }
            return MappingProviderFactory.createProvider().convertValue(modelRef, Set.class, targetClass);
        }

        @Override
        public <T> T to(Class<T> targetClass) {
            return MappingProviderFactory.createProvider().convertValue(model, targetClass);
        }
    }

    private static class JsonSubModel extends JsonModel {

        private final JsonModel parent;
        private final JsonPath subModelPath;

        private JsonSubModel(Object jsonObject, JsonProvider jsonProvider, JsonModel parent,
                JsonPath subModelPath) {
            super(jsonObject, jsonProvider);
            this.parent = parent;
            this.subModelPath = subModelPath;
        }
    }
}