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    void addChildObject(String fieldName, BoxJSONObject child) {
142        if (child == null) {
143            this.addPendingChange(fieldName, JsonValue.NULL);
144        } else {
145            this.children.put(fieldName, child);
146        }
147    }
148
149    void removeChildObject(String fieldName) {
150        this.children.remove(fieldName);
151    }
152
153    /**
154     * Adds a pending field change that needs to be sent to the API. It will be included in the JSON string the next
155     * time {@link #getPendingChanges} is called.
156     * @param key   the name of the field.
157     * @param value the JsonValue of the field.
158     */
159    private void addPendingChange(String key, JsonValue value) {
160        if (this.pendingChanges == null) {
161            this.pendingChanges = new JsonObject();
162        }
163
164        this.pendingChanges.set(key, value);
165    }
166
167    void removePendingChange(String key) {
168        if (this.pendingChanges != null) {
169            this.pendingChanges.remove(key);
170        }
171    }
172
173    /**
174     * Updates this BoxJSONObject using the information in a JSON object.
175     * @param jsonObject the JSON object containing updated information.
176     */
177    void update(JsonObject jsonObject) {
178        for (JsonObject.Member member : jsonObject) {
179            if (member.getValue().isNull()) {
180                continue;
181            }
182
183            this.parseJSONMember(member);
184        }
185
186        this.clearPendingChanges();
187    }
188
189    /**
190     * Gets a JsonObject containing any pending changes to this object that can be sent back to the Box API.
191     * @return a JsonObject containing the pending changes.
192     */
193    private JsonObject getPendingJSONObject() {
194        for (Map.Entry<String, BoxJSONObject> entry : this.children.entrySet()) {
195            BoxJSONObject child = entry.getValue();
196            JsonObject jsonObject = child.getPendingJSONObject();
197            if (jsonObject != null) {
198                if (this.pendingChanges == null) {
199                    this.pendingChanges = new JsonObject();
200                }
201
202                this.pendingChanges.set(entry.getKey(), jsonObject);
203            }
204        }
205        return this.pendingChanges;
206    }
207}