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