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