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