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     * A map of other BoxJSONObjects which will be lazily converted to a JsonObject once getPendingChanges is called.
025     * This allows changes to be made to a child BoxJSONObject and still have those changes reflected in the JSON
026     * string.
027     */
028    private final Map<String, BoxJSONObject> children;
029
030    /**
031     * Constructs an empty BoxJSONObject.
032     */
033    public BoxJSONObject() {
034        this.children = new HashMap<String, BoxJSONObject>();
035    }
036
037    /**
038     * Constructs a BoxJSONObject by decoding it from a JSON string.
039     * @param  json the JSON string to decode.
040     */
041    public BoxJSONObject(String json) {
042        this(JsonObject.readFrom(json));
043    }
044
045    /**
046     * Constructs a BoxJSONObject using an already parsed JSON object.
047     * @param  jsonObject the parsed JSON object.
048     */
049    BoxJSONObject(JsonObject jsonObject) {
050        this();
051
052        this.update(jsonObject);
053    }
054
055    /**
056     * Clears any pending changes from this JSON object.
057     */
058    public void clearPendingChanges() {
059        this.pendingChanges = null;
060    }
061
062    /**
063     * Gets a JSON string containing any pending changes to this object that can be sent back to the Box API.
064     * @return a JSON string containing the pending changes.
065     */
066    public String getPendingChanges() {
067        JsonObject jsonObject = this.getPendingJSONObject();
068        if (jsonObject == null) {
069            return null;
070        }
071
072        return jsonObject.toString();
073    }
074
075    /**
076     * Gets a JSON string containing any pending changes to this object that can be sent back to the Box API.
077     * @return a JSON string containing the pending changes.
078     */
079    public JsonObject getPendingChangesAsJsonObject() {
080        JsonObject jsonObject = this.getPendingJSONObject();
081        if (jsonObject == null) {
082            return null;
083        }
084        return jsonObject;
085    }
086
087    /**
088     * Invoked with a JSON member whenever this object is updated or created from a JSON object.
089     *
090     * <p>Subclasses should override this method in order to parse any JSON members it knows about. This method is a
091     * no-op by default.</p>
092     *
093     * @param member the JSON member to be parsed.
094     */
095    void parseJSONMember(JsonObject.Member member) { }
096
097    /**
098     * Adds a pending field change that needs to be sent to the API. It will be included in the JSON string the next
099     * time {@link #getPendingChanges} is called.
100     * @param key   the name of the field.
101     * @param value the new boolean value of the field.
102     */
103    void addPendingChange(String key, boolean value) {
104        if (this.pendingChanges == null) {
105            this.pendingChanges = new JsonObject();
106        }
107
108        this.pendingChanges.set(key, value);
109    }
110
111    /**
112     * Adds a pending field change that needs to be sent to the API. It will be included in the JSON string the next
113     * time {@link #getPendingChanges} is called.
114     * @param key   the name of the field.
115     * @param value the new String value of the field.
116     */
117    void addPendingChange(String key, String value) {
118        this.addPendingChange(key, JsonValue.valueOf(value));
119    }
120
121    /**
122     * Adds a pending field change that needs to be sent to the API. It will be included in the JSON string the next
123     * time {@link #getPendingChanges} is called.
124     * @param key   the name of the field.
125     * @param value the new long value of the field.
126     */
127    void addPendingChange(String key, long value) {
128        this.addPendingChange(key, JsonValue.valueOf(value));
129    }
130
131    /**
132     * Adds a pending field change that needs to be sent to the API. It will be included in the JSON string the next
133     * time {@link #getPendingChanges} is called.
134     * @param key   the name of the field.
135     * @param value the new JsonArray value of the field.
136     */
137    void addPendingChange(String key, JsonArray value) {
138        this.addPendingChange(key, (JsonValue) value);
139    }
140
141    /**
142     * Adds a pending field change that needs to be sent to the API. It will be included in the JSON string the next
143     * time {@link #getPendingChanges} is called.
144     * @param key   the name of the field.
145     * @param value the new JsonObject value of the field.
146     */
147    void addPendingChange(String key, JsonObject value) {
148        this.addPendingChange(key, (JsonValue) value);
149    }
150
151    void addChildObject(String fieldName, BoxJSONObject child) {
152        if (child == null) {
153            this.addPendingChange(fieldName, JsonValue.NULL);
154        } else {
155            this.children.put(fieldName, child);
156        }
157    }
158
159    void removeChildObject(String fieldName) {
160        this.children.remove(fieldName);
161    }
162
163    /**
164     * Adds a pending field change that needs to be sent to the API. It will be included in the JSON string the next
165     * time {@link #getPendingChanges} is called.
166     * @param key   the name of the field.
167     * @param value the JsonValue of the field.
168     */
169    private void addPendingChange(String key, JsonValue value) {
170        if (this.pendingChanges == null) {
171            this.pendingChanges = new JsonObject();
172        }
173
174        this.pendingChanges.set(key, value);
175    }
176
177    void removePendingChange(String key) {
178        if (this.pendingChanges != null) {
179            this.pendingChanges.remove(key);
180        }
181    }
182
183    /**
184     * Updates this BoxJSONObject using the information in a JSON object.
185     * @param jsonObject the JSON object containing updated information.
186     */
187    void update(JsonObject jsonObject) {
188        for (JsonObject.Member member : jsonObject) {
189            if (member.getValue().isNull()) {
190                continue;
191            }
192
193            this.parseJSONMember(member);
194        }
195
196        this.clearPendingChanges();
197    }
198
199    /**
200     * Gets a JsonObject containing any pending changes to this object that can be sent back to the Box API.
201     * @return a JsonObject containing the pending changes.
202     */
203    private JsonObject getPendingJSONObject() {
204        for (Map.Entry<String, BoxJSONObject> entry : this.children.entrySet()) {
205            BoxJSONObject child = entry.getValue();
206            JsonObject jsonObject = child.getPendingJSONObject();
207            if (jsonObject != null) {
208                if (this.pendingChanges == null) {
209                    this.pendingChanges = new JsonObject();
210                }
211
212                this.pendingChanges.set(entry.getKey(), jsonObject);
213            }
214        }
215        return this.pendingChanges;
216    }
217}