001package com.box.sdk;
002
003import java.util.HashMap;
004import java.util.Map;
005
006import com.eclipsesource.json.JsonObject;
007import com.eclipsesource.json.JsonValue;
008
009/**
010 * The abstract base class for all types that contain JSON data returned by the Box API. The most common implementation
011 * of BoxJSONObject is {@link BoxResource.Info} and its subclasses. Changes made to a BoxJSONObject will be tracked
012 * locally until the pending changes are sent back to Box in order to avoid unnecessary network requests.
013 *
014 */
015public abstract class BoxJSONObject {
016    /**
017     * The JsonObject that contains any local pending changes. When getPendingChanges is called, this object will be
018     * encoded to a JSON string.
019     */
020    private JsonObject pendingChanges;
021
022    /**
023     * A map of other BoxJSONObjects which will be lazily converted to a JsonObject once getPendingChanges is called.
024     * This allows changes to be made to a child BoxJSONObject and still have those changes reflected in the JSON
025     * string.
026     */
027    private final Map<String, BoxJSONObject> children;
028
029    /**
030     * Constructs an empty BoxJSONObject.
031     */
032    public BoxJSONObject() {
033        this.children = new HashMap<String, BoxJSONObject>();
034    }
035
036    /**
037     * Constructs a BoxJSONObject by decoding it from a JSON string.
038     * @param  json the JSON string to decode.
039     */
040    public BoxJSONObject(String json) {
041        this(JsonObject.readFrom(json));
042    }
043
044    /**
045     * Constructs a BoxJSONObject using an already parsed JSON object.
046     * @param  jsonObject the parsed JSON object.
047     */
048    BoxJSONObject(JsonObject jsonObject) {
049        this();
050
051        this.update(jsonObject);
052    }
053
054    /**
055     * Clears any pending changes from this JSON object.
056     */
057    public void clearPendingChanges() {
058        this.pendingChanges = null;
059    }
060
061    /**
062     * Gets a JSON string containing any pending changes to this object that can be sent back to the Box API.
063     * @return a JSON string containing the pending changes.
064     */
065    public String getPendingChanges() {
066        JsonObject jsonObject = this.getPendingJSONObject();
067        if (jsonObject == null) {
068            return null;
069        }
070
071        return jsonObject.toString();
072    }
073
074    /**
075     * Invoked with a JSON member whenever this object is updated or created from a JSON object.
076     *
077     * <p>Subclasses should override this method in order to parse any JSON members it knows about. This method is a
078     * no-op by default.</p>
079     *
080     * @param member the JSON member to be parsed.
081     */
082    void parseJSONMember(JsonObject.Member member) { }
083
084    /**
085     * Adds a pending field change that needs to be sent to the API. It will be included in the JSON string the next
086     * time {@link #getPendingChanges} is called.
087     * @param key   the name of the field.
088     * @param value the new boolean value of the field.
089     */
090    void addPendingChange(String key, boolean value) {
091        if (this.pendingChanges == null) {
092            this.pendingChanges = new JsonObject();
093        }
094
095        this.pendingChanges.set(key, value);
096    }
097
098    /**
099     * Adds a pending field change that needs to be sent to the API. It will be included in the JSON string the next
100     * time {@link #getPendingChanges} is called.
101     * @param key   the name of the field.
102     * @param value the new String value of the field.
103     */
104    void addPendingChange(String key, String value) {
105        this.addPendingChange(key, JsonValue.valueOf(value));
106    }
107
108    /**
109     * Adds a pending field change that needs to be sent to the API. It will be included in the JSON string the next
110     * time {@link #getPendingChanges} is called.
111     * @param key   the name of the field.
112     * @param value the new long value of the field.
113     */
114    void addPendingChange(String key, long value) {
115        this.addPendingChange(key, JsonValue.valueOf(value));
116    }
117
118    void addChildObject(String fieldName, BoxJSONObject child) {
119        if (child == null) {
120            this.addPendingChange(fieldName, JsonValue.NULL);
121        } else {
122            this.children.put(fieldName, child);
123        }
124    }
125
126    void removeChildObject(String fieldName) {
127        this.children.remove(fieldName);
128    }
129
130    /**
131     * Adds a pending field change that needs to be sent to the API. It will be included in the JSON string the next
132     * time {@link #getPendingChanges} is called.
133     * @param key   the name of the field.
134     * @param value the JsonValue of the field.
135     */
136    private void addPendingChange(String key, JsonValue value) {
137        if (this.pendingChanges == null) {
138            this.pendingChanges = new JsonObject();
139        }
140
141        this.pendingChanges.set(key, value);
142    }
143
144    void removePendingChange(String key) {
145        if (this.pendingChanges != null) {
146            this.pendingChanges.remove(key);
147        }
148    }
149
150    /**
151     * Updates this BoxJSONObject using the information in a JSON object.
152     * @param jsonObject the JSON object containing updated information.
153     */
154    void update(JsonObject jsonObject) {
155        for (JsonObject.Member member : jsonObject) {
156            if (member.getValue().isNull()) {
157                continue;
158            }
159
160            this.parseJSONMember(member);
161        }
162
163        this.clearPendingChanges();
164    }
165
166    /**
167     * Gets a JsonObject containing any pending changes to this object that can be sent back to the Box API.
168     * @return a JsonObject containing the pending changes.
169     */
170    private JsonObject getPendingJSONObject() {
171        for (Map.Entry<String, BoxJSONObject> entry : this.children.entrySet()) {
172            BoxJSONObject child = entry.getValue();
173            JsonObject jsonObject = child.getPendingJSONObject();
174            if (jsonObject != null) {
175                if (this.pendingChanges == null) {
176                    this.pendingChanges = new JsonObject();
177                }
178
179                this.pendingChanges.set(entry.getKey(), jsonObject);
180            }
181        }
182        return this.pendingChanges;
183    }
184}