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     */
182    public Metadata add(String path, float value) {
183        this.values.add(this.pathToProperty(path), value);
184        this.addOp("add", path, value);
185        return this;
186    }
187
188    /**
189     * Adds a new metadata value of array type.
190     * @param path the path to the field.
191     * @param values the collection of values.
192     * @return the metadata object for chaining.
193     */
194    public Metadata add(String path, List<String> values) {
195        JsonArray arr = new JsonArray();
196        for (String value : values) {
197            arr.add(value);
198        }
199        this.values.add(this.pathToProperty(path), arr);
200        this.addOp("add", path, arr);
201        return this;
202    }
203
204    /**
205     * Replaces an existing metadata value.
206     * @param path the path that designates the key. Must be prefixed with a "/".
207     * @param value the value.
208     * @return this metadata object.
209     */
210    public Metadata replace(String path, String value) {
211        this.values.set(this.pathToProperty(path), value);
212        this.addOp("replace", path, value);
213        return this;
214    }
215
216    /**
217     * Replaces an existing metadata value.
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 of array type.
230     * @param path the path that designates the key. Must be prefixed with a "/".
231     * @param values the collection of values.
232     * @return the metadata object.
233     */
234    public Metadata replace(String path, List<String> values) {
235        JsonArray arr = new JsonArray();
236        for (String value : values) {
237            arr.add(value);
238        }
239        this.values.add(this.pathToProperty(path), arr);
240        this.addOp("replace", path, arr);
241        return this;
242    }
243
244    /**
245     * Removes an existing metadata value.
246     * @param path the path that designates the key. Must be prefixed with a "/".
247     * @return this metadata object.
248     */
249    public Metadata remove(String path) {
250        this.values.remove(this.pathToProperty(path));
251        this.addOp("remove", path, (String) null);
252        return this;
253    }
254
255    /**
256     * Tests that a property has the expected value.
257     * @param path the path that designates the key. Must be prefixed with a "/".
258     * @param value the expected value.
259     * @return this metadata object.
260     */
261    public Metadata test(String path, String value) {
262        this.addOp("test", path, value);
263        return this;
264    }
265
266    /**
267     * Tests that a list of properties has the expected value.
268     * The values passed in will have to be an exact match with no extra elements.
269     * @param path      the path that designates the key. Must be prefixed with a "/".
270     * @param values    the list of expected values.
271     * @return          this metadata object.
272     */
273    public Metadata test(String path, List<String> values) {
274        JsonArray arr = new JsonArray();
275        for (String value : values) {
276            arr.add(value);
277        }
278        this.addOp("test", path, arr);
279        return this;
280    }
281
282    /**
283     * Returns a value.
284     * @param path the path that designates the key. Must be prefixed with a "/".
285     * @return the metadata property value.
286     * @deprecated Metadata#get() does not handle all possible metadata types; use Metadata#getValue() instead
287     */
288    @Deprecated
289    public String get(String path) {
290        final JsonValue value = this.values.get(this.pathToProperty(path));
291        if (value == null) {
292            return null;
293        }
294        if (!value.isString()) {
295            return value.toString();
296        }
297        return value.asString();
298    }
299
300    /**
301     * Returns a value, regardless of type.
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.
311     * @param path the key path in the metadata object.  Must be prefixed with a "/".
312     * @return the metadata value as a string.
313     */
314    public String getString(String path) {
315        return this.getValue(path).asString();
316    }
317
318    /**
319     * Get a value from a float metadata field.
320     * @param path the key path in the metadata object.  Must be prefixed with a "/".
321     * @return the metadata value as a floating point number.
322     */
323    public double getFloat(String path) {
324        // @NOTE(mwiller) 2018-02-05: JS number are all 64-bit floating point, so double is the correct type to use here
325        return this.getValue(path).asDouble();
326    }
327
328    /**
329     * Get a value from a date metadata field.
330     * @param path the key path in the metadata object.  Must be prefixed with a "/".
331     * @return the metadata value as a Date.
332     * @throws ParseException when the value cannot be parsed as a valid date
333     */
334    public Date getDate(String path) throws ParseException {
335        return BoxDateFormat.parse(this.getValue(path).asString());
336    }
337
338    /**
339     * Get a value from a multiselect metadata field.
340     * @param path the key path in the metadata object.  Must be prefixed with a "/".
341     * @return the list of values set in the field.
342     */
343    public List<String> getMultiSelect(String path) {
344        List<String> values = new ArrayList<String>();
345        for (JsonValue val : this.getValue(path).asArray()) {
346            values.add(val.asString());
347        }
348
349        return values;
350    }
351
352    /**
353     * Returns a list of metadata property paths.
354     * @return the list of metdata property paths.
355     */
356    public List<String> getPropertyPaths() {
357        List<String> result = new ArrayList<String>();
358
359        for (String property : this.values.names()) {
360            if (!property.startsWith("$")) {
361                result.add(this.propertyToPath(property));
362            }
363        }
364
365        return result;
366    }
367
368    /**
369     * Returns the JSON patch string with all operations.
370     * @return the JSON patch string.
371     */
372    public String getPatch() {
373        if (this.operations == null) {
374            return "[]";
375        }
376        return this.operations.toString();
377    }
378
379    /**
380     * Returns an array of operations on metadata.
381     * @return a JSON array of operations.
382     */
383    public JsonArray getOperations() {
384        return this.operations;
385    }
386
387    /**
388     * Returns the JSON representation of this metadata.
389     * @return the JSON representation of this metadata.
390     */
391    @Override
392    public String toString() {
393        return this.values.toString();
394    }
395
396    /**
397     * Converts a JSON patch path to a JSON property name.
398     * Currently the metadata API only supports flat maps.
399     * @param path the path that designates the key.  Must be prefixed with a "/".
400     * @return the JSON property name.
401     */
402    private String pathToProperty(String path) {
403        if (path == null || !path.startsWith("/")) {
404            throw new IllegalArgumentException("Path must be prefixed with a \"/\".");
405        }
406        return path.substring(1);
407    }
408
409    /**
410     * Converts a JSON property name to a JSON patch path.
411     * @param property the JSON property name.
412     * @return the path that designates the key.
413     */
414    private String propertyToPath(String property) {
415        if (property == null) {
416            throw new IllegalArgumentException("Property must not be null.");
417        }
418        return "/" + property;
419    }
420
421    /**
422     * Adds a patch operation.
423     * @param op the operation type. Must be add, replace, remove, or test.
424     * @param path the path that designates the key. Must be prefixed with a "/".
425     * @param value the value to be set.
426     */
427    private void addOp(String op, String path, String value) {
428        if (this.operations == null) {
429            this.operations = new JsonArray();
430        }
431
432        this.operations.add(new JsonObject()
433                .add("op", op)
434                .add("path", path)
435                .add("value", value));
436    }
437
438    /**
439     * Adds a patch operation.
440     * @param op the operation type. Must be add, replace, remove, or test.
441     * @param path the path that designates the key. Must be prefixed with a "/".
442     * @param value the value to be set.
443     */
444    private void addOp(String op, String path, float value) {
445        if (this.operations == null) {
446            this.operations = new JsonArray();
447        }
448
449        this.operations.add(new JsonObject()
450                .add("op", op)
451                .add("path", path)
452                .add("value", value));
453    }
454
455    /**
456     * Adds a new patch operation for array values.
457     * @param op the operation type. Must be add, replace, remove, or test.
458     * @param path the path that designates the key. Must be prefixed with a "/".
459     * @param values the array of values to be set.
460     */
461    private void addOp(String op, String path, JsonArray values) {
462
463        if (this.operations == null) {
464            this.operations = new JsonArray();
465        }
466
467        this.operations.add(new JsonObject()
468                .add("op", op)
469                .add("path", path)
470                .add("value", values));
471    }
472
473    static String scopeBasedOnType(String typeName) {
474        String scope;
475        if (typeName.equals(DEFAULT_METADATA_TYPE)) {
476            scope = GLOBAL_METADATA_SCOPE;
477        } else {
478            scope = ENTERPRISE_METADATA_SCOPE;
479        }
480        return scope;
481    }
482}