001package com.box.sdk;
002
003import com.eclipsesource.json.JsonArray;
004import com.eclipsesource.json.JsonObject;
005import com.eclipsesource.json.JsonValue;
006import java.text.ParseException;
007import java.util.ArrayList;
008import java.util.Date;
009import java.util.List;
010import java.util.Optional;
011
012/**
013 * The Metadata class represents one type instance of Box metadata.
014 *
015 * <p>Learn more about Box metadata: https://developers.box.com/metadata-api/
016 */
017public class Metadata {
018
019  /** Specifies the name of the default "properties" metadata template. */
020  public static final String DEFAULT_METADATA_TYPE = "properties";
021
022  /** Specifies the "global" metadata scope. */
023  public static final String GLOBAL_METADATA_SCOPE = "global";
024
025  /** Specifies the "enterprise" metadata scope. */
026  public static final String ENTERPRISE_METADATA_SCOPE = "enterprise";
027
028  /** Specifies the classification template key. */
029  public static final String CLASSIFICATION_TEMPLATE_KEY = "securityClassification-6VMVochwUWo";
030
031  /** Classification key path. */
032  public static final String CLASSIFICATION_KEY = "/Box__Security__Classification__Key";
033
034  /** The default limit of entries per response. */
035  public static final int DEFAULT_LIMIT = 100;
036
037  /** URL template for all metadata associated with item. */
038  public static final URLTemplate GET_ALL_METADATA_URL_TEMPLATE = new URLTemplate("/metadata");
039
040  /** Values contained by the metadata object. */
041  private final JsonObject values;
042
043  /** Operations to be applied to the metadata object. */
044  private JsonArray operations = new JsonArray();
045
046  /** Creates an empty metadata. */
047  public Metadata() {
048    this.values = new JsonObject();
049  }
050
051  /**
052   * Creates a new metadata.
053   *
054   * @param values the initial metadata values.
055   */
056  public Metadata(JsonObject values) {
057    this.values = values;
058  }
059
060  /**
061   * Creates a copy of another metadata.
062   *
063   * @param other the other metadata object to copy.
064   */
065  public Metadata(Metadata other) {
066    this.values = new JsonObject(other.values);
067  }
068
069  /**
070   * Creates a new metadata with the specified scope and template.
071   *
072   * @param scope the scope of the metadata.
073   * @param template the template of the metadata.
074   */
075  public Metadata(String scope, String template) {
076    this.values = new JsonObject().add("$scope", scope).add("$template", template);
077  }
078
079  /**
080   * Used to retrieve all metadata associated with the item.
081   *
082   * @param item item to get metadata for.
083   * @param fields the optional fields to retrieve.
084   * @return An iterable of metadata instances associated with the item.
085   */
086  public static Iterable<Metadata> getAllMetadata(BoxItem item, String... fields) {
087    QueryStringBuilder builder = new QueryStringBuilder();
088    if (fields.length > 0) {
089      builder.appendParam("fields", fields);
090    }
091    return new BoxResourceIterable<Metadata>(
092        item.getAPI(),
093        GET_ALL_METADATA_URL_TEMPLATE.buildWithQuery(
094            item.getItemURL().toString(), builder.toString()),
095        DEFAULT_LIMIT) {
096
097      @Override
098      protected Metadata factory(JsonObject jsonObject) {
099        return new Metadata(jsonObject);
100      }
101    };
102  }
103
104  static String scopeBasedOnType(String typeName) {
105    String scope;
106    if (typeName.equals(DEFAULT_METADATA_TYPE)) {
107      scope = GLOBAL_METADATA_SCOPE;
108    } else {
109      scope = ENTERPRISE_METADATA_SCOPE;
110    }
111    return scope;
112  }
113
114  /**
115   * Returns the 36 character UUID to identify the metadata object.
116   *
117   * @return the metadata ID.
118   */
119  public String getID() {
120    return getStringOrNull("/$id");
121  }
122
123  /**
124   * Returns the metadata type.
125   *
126   * @return the metadata type.
127   */
128  public String getTypeName() {
129    return getStringOrNull("/$type");
130  }
131
132  /**
133   * Returns the parent object ID (typically the file ID).
134   *
135   * @return the parent object ID.
136   */
137  public String getParentID() {
138    return getStringOrNull("/$parent");
139  }
140
141  /**
142   * Returns the scope. Can throw {@link NullPointerException} is value if not present.
143   *
144   * @return the scope.
145   */
146  public String getScope() {
147    return getStringOrNull("/$scope");
148  }
149
150  /**
151   * Returns the template name. Can throw {@link NullPointerException} is value if not present.
152   *
153   * @return the template name.
154   */
155  public String getTemplateName() {
156    return getStringOrNull("/$template");
157  }
158
159  /**
160   * Adds a new metadata value.
161   *
162   * @param path the path that designates the key. Must be prefixed with a "/".
163   * @param value the value.
164   * @return this metadata object.
165   */
166  public Metadata add(String path, String value) {
167    this.values.add(this.pathToProperty(path), value);
168    this.addOp("add", path, value);
169    return this;
170  }
171
172  /**
173   * Adds a new metadata value.
174   *
175   * @param path the path that designates the key. Must be prefixed with a "/".
176   * @param value the value.
177   * @return this metadata object.
178   */
179  public Metadata add(String path, double value) {
180    this.values.add(this.pathToProperty(path), value);
181    this.addOp("add", path, value);
182    return this;
183  }
184
185  /**
186   * Adds a new metadata value of array type.
187   *
188   * @param path the path to the field.
189   * @param values the collection of values.
190   * @return the metadata object for chaining.
191   */
192  public Metadata add(String path, List<String> values) {
193    JsonArray arr = new JsonArray();
194    for (String value : values) {
195      arr.add(value);
196    }
197    this.values.add(this.pathToProperty(path), arr);
198    this.addOp("add", path, arr);
199    return this;
200  }
201
202  /**
203   * Replaces an existing metadata value.
204   *
205   * @param path the path that designates the key. Must be prefixed with a "/".
206   * @param value the value.
207   * @return this metadata object.
208   */
209  public Metadata replace(String path, String value) {
210    this.values.set(this.pathToProperty(path), value);
211    this.addOp("replace", path, value);
212    return this;
213  }
214
215  /**
216   * Replaces an existing metadata value.
217   *
218   * @param path the path that designates the key. Must be prefixed with a "/".
219   * @param value the value.
220   * @return this metadata object.
221   */
222  public Metadata replace(String path, float value) {
223    this.values.set(this.pathToProperty(path), value);
224    this.addOp("replace", path, value);
225    return this;
226  }
227
228  /**
229   * Replaces an existing metadata value.
230   *
231   * @param path the path that designates the key. Must be prefixed with a "/".
232   * @param value the value.
233   * @return this metadata object.
234   */
235  public Metadata replace(String path, double value) {
236    this.values.set(this.pathToProperty(path), value);
237    this.addOp("replace", path, value);
238    return this;
239  }
240
241  /**
242   * Replaces an existing metadata value of array type.
243   *
244   * @param path the path that designates the key. Must be prefixed with a "/".
245   * @param values the collection of values.
246   * @return the metadata object.
247   */
248  public Metadata replace(String path, List<String> values) {
249    JsonArray arr = new JsonArray();
250    for (String value : values) {
251      arr.add(value);
252    }
253    this.values.add(this.pathToProperty(path), arr);
254    this.addOp("replace", path, arr);
255    return this;
256  }
257
258  /**
259   * Removes an existing metadata value.
260   *
261   * @param path the path that designates the key. Must be prefixed with a "/".
262   * @return this metadata object.
263   */
264  public Metadata remove(String path) {
265    this.values.remove(this.pathToProperty(path));
266    this.addOp("remove", path, (String) null);
267    return this;
268  }
269
270  /**
271   * Tests that a property has the expected value.
272   *
273   * @param path the path that designates the key. Must be prefixed with a "/".
274   * @param value the expected value.
275   * @return this metadata object.
276   */
277  public Metadata test(String path, String value) {
278    this.addOp("test", path, value);
279    return this;
280  }
281
282  /**
283   * Tests that a list of properties has the expected value. The values passed in will have to be an
284   * exact match with no extra elements.
285   *
286   * @param path the path that designates the key. Must be prefixed with a "/".
287   * @param values the list of expected values.
288   * @return this metadata object.
289   */
290  public Metadata test(String path, List<String> values) {
291    JsonArray arr = new JsonArray();
292    for (String value : values) {
293      arr.add(value);
294    }
295    this.addOp("test", path, arr);
296    return this;
297  }
298
299  /**
300   * Returns a value, regardless of type.
301   *
302   * @param path the path that designates the key. Must be prefixed with a "/".
303   * @return the metadata property value as an indeterminate JSON type.
304   */
305  public JsonValue getValue(String path) {
306    return this.values.get(this.pathToProperty(path));
307  }
308
309  /**
310   * Get a value from a string or enum metadata field. Can throw {@link NullPointerException} is
311   * value if not present.
312   *
313   * @param path the key path in the metadata object. Must be prefixed with a "/".
314   * @return the metadata value as a string.
315   */
316  public String getString(String path) {
317    return this.getValue(path).asString();
318  }
319
320  /**
321   * Get a value from a double metadata field. Can throw {@link NullPointerException} is value if
322   * not present.
323   *
324   * @param path the key path in the metadata object. Must be prefixed with a "/".
325   * @return the metadata value as a floating point number.
326   */
327  public double getDouble(String path) {
328    return this.getValue(path).asDouble();
329  }
330
331  /**
332   * Get a value from a date metadata field. Can throw {@link NullPointerException} is value if not
333   * present.
334   *
335   * @param path the key path in the metadata object. Must be prefixed with a "/".
336   * @return the metadata value as a Date.
337   * @throws ParseException when the value cannot be parsed as a valid date
338   */
339  public Date getDate(String path) throws ParseException {
340    return BoxDateFormat.parse(this.getValue(path).asString());
341  }
342
343  /**
344   * Get a value from a multiselect metadata field.
345   *
346   * @param path the key path in the metadata object. Must be prefixed with a "/".
347   * @return the list of values set in the field.
348   */
349  public List<String> getMultiSelect(String path) {
350    List<String> values = new ArrayList<>();
351    for (JsonValue val : this.getValue(path).asArray()) {
352      values.add(val.asString());
353    }
354
355    return values;
356  }
357
358  /**
359   * Returns a list of metadata property paths.
360   *
361   * @return the list of metdata property paths.
362   */
363  public List<String> getPropertyPaths() {
364    List<String> result = new ArrayList<>();
365
366    for (String property : this.values.names()) {
367      if (!property.startsWith("$")) {
368        result.add(this.propertyToPath(property));
369      }
370    }
371
372    return result;
373  }
374
375  /**
376   * Returns the JSON patch string with all operations.
377   *
378   * @return the JSON patch string.
379   */
380  public String getPatch() {
381    if (this.operations == null) {
382      return "[]";
383    }
384    return this.operations.toString();
385  }
386
387  /**
388   * Returns an array of operations on metadata.
389   *
390   * @return a JSON array of operations.
391   */
392  public JsonArray getOperations() {
393    return this.operations;
394  }
395
396  /**
397   * Returns the JSON representation of this metadata.
398   *
399   * @return the JSON representation of this metadata.
400   */
401  @Override
402  public String toString() {
403    return this.values.toString();
404  }
405
406  /**
407   * Converts a JSON patch path to a JSON property name. Currently the metadata API only supports
408   * flat maps.
409   *
410   * @param path the path that designates the key. Must be prefixed with a "/".
411   * @return the JSON property name.
412   */
413  private String pathToProperty(String path) {
414    if (path == null || !path.startsWith("/")) {
415      throw new IllegalArgumentException("Path must be prefixed with a \"/\".");
416    }
417    return path.substring(1);
418  }
419
420  /**
421   * Converts a JSON property name to a JSON patch path.
422   *
423   * @param property the JSON property name.
424   * @return the path that designates the key.
425   */
426  private String propertyToPath(String property) {
427    if (property == null) {
428      throw new IllegalArgumentException("Property must not be null.");
429    }
430    return "/" + property;
431  }
432
433  /**
434   * Adds a patch operation.
435   *
436   * @param op the operation type. Must be add, replace, remove, or test.
437   * @param path the path that designates the key. Must be prefixed with a "/".
438   * @param value the value to be set.
439   */
440  private void addOp(String op, String path, String value) {
441    if (this.operations == null) {
442      this.operations = new JsonArray();
443    }
444
445    this.operations.add(new JsonObject().add("op", op).add("path", path).add("value", value));
446  }
447
448  /**
449   * Adds a patch operation.
450   *
451   * @param op the operation type. Must be add, replace, remove, or test.
452   * @param path the path that designates the key. Must be prefixed with a "/".
453   * @param value the value to be set.
454   */
455  private void addOp(String op, String path, double value) {
456    if (this.operations == null) {
457      this.operations = new JsonArray();
458    }
459
460    this.operations.add(new JsonObject().add("op", op).add("path", path).add("value", value));
461  }
462
463  /**
464   * Adds a new patch operation for array values.
465   *
466   * @param op the operation type. Must be add, replace, remove, or test.
467   * @param path the path that designates the key. Must be prefixed with a "/".
468   * @param values the array of values to be set.
469   */
470  private void addOp(String op, String path, JsonArray values) {
471
472    if (this.operations == null) {
473      this.operations = new JsonArray();
474    }
475
476    this.operations.add(new JsonObject().add("op", op).add("path", path).add("value", values));
477  }
478
479  private String getStringOrNull(String path) {
480    return Optional.ofNullable(this.getValue(path)).map(JsonValue::asString).orElse(null);
481  }
482}