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