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