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