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     * Invoked with a JSON member whenever this object is updated or created from a JSON object.
077     *
078     * <p>Subclasses should override this method in order to parse any JSON members it knows about. This method is a
079     * no-op by default.</p>
080     *
081     * @param member the JSON member to be parsed.
082     */
083    void parseJSONMember(JsonObject.Member member) { }
084
085    /**
086     * Adds a pending field change that needs to be sent to the API. It will be included in the JSON string the next
087     * time {@link #getPendingChanges} is called.
088     * @param key   the name of the field.
089     * @param value the new boolean value of the field.
090     */
091    void addPendingChange(String key, boolean value) {
092        if (this.pendingChanges == null) {
093            this.pendingChanges = new JsonObject();
094        }
095
096        this.pendingChanges.set(key, value);
097    }
098
099    /**
100     * Adds a pending field change that needs to be sent to the API. It will be included in the JSON string the next
101     * time {@link #getPendingChanges} is called.
102     * @param key   the name of the field.
103     * @param value the new String value of the field.
104     */
105    void addPendingChange(String key, String value) {
106        this.addPendingChange(key, JsonValue.valueOf(value));
107    }
108
109    /**
110     * Adds a pending field change that needs to be sent to the API. It will be included in the JSON string the next
111     * time {@link #getPendingChanges} is called.
112     * @param key   the name of the field.
113     * @param value the new long value of the field.
114     */
115    void addPendingChange(String key, long value) {
116        this.addPendingChange(key, JsonValue.valueOf(value));
117    }
118
119    /**
120     * Adds a pending field change that needs to be sent to the API. It will be included in the JSON string the next
121     * time {@link #getPendingChanges} is called.
122     * @param key   the name of the field.
123     * @param value the new JsonArray value of the field.
124     */
125    void addPendingChange(String key, JsonArray value) {
126        this.addPendingChange(key, (JsonValue) value);
127    }
128
129    void addChildObject(String fieldName, BoxJSONObject child) {
130        if (child == null) {
131            this.addPendingChange(fieldName, JsonValue.NULL);
132        } else {
133            this.children.put(fieldName, child);
134        }
135    }
136
137    void removeChildObject(String fieldName) {
138        this.children.remove(fieldName);
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 JsonValue of the field.
146     */
147    private void addPendingChange(String key, JsonValue value) {
148        if (this.pendingChanges == null) {
149            this.pendingChanges = new JsonObject();
150        }
151
152        this.pendingChanges.set(key, value);
153    }
154
155    void removePendingChange(String key) {
156        if (this.pendingChanges != null) {
157            this.pendingChanges.remove(key);
158        }
159    }
160
161    /**
162     * Updates this BoxJSONObject using the information in a JSON object.
163     * @param jsonObject the JSON object containing updated information.
164     */
165    void update(JsonObject jsonObject) {
166        for (JsonObject.Member member : jsonObject) {
167            if (member.getValue().isNull()) {
168                continue;
169            }
170
171            this.parseJSONMember(member);
172        }
173
174        this.clearPendingChanges();
175    }
176
177    /**
178     * Gets a JsonObject containing any pending changes to this object that can be sent back to the Box API.
179     * @return a JsonObject containing the pending changes.
180     */
181    private JsonObject getPendingJSONObject() {
182        for (Map.Entry<String, BoxJSONObject> entry : this.children.entrySet()) {
183            BoxJSONObject child = entry.getValue();
184            JsonObject jsonObject = child.getPendingJSONObject();
185            if (jsonObject != null) {
186                if (this.pendingChanges == null) {
187                    this.pendingChanges = new JsonObject();
188                }
189
190                this.pendingChanges.set(entry.getKey(), jsonObject);
191            }
192        }
193        return this.pendingChanges;
194    }
195}