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 * <p>
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 = new JsonArray();
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     *
075     * @param values the initial metadata values.
076     */
077    public Metadata(JsonObject values) {
078        this.values = values;
079    }
080
081    /**
082     * Creates a copy of another metadata.
083     *
084     * @param other the other metadata object to copy.
085     */
086    public Metadata(Metadata other) {
087        this.values = new JsonObject(other.values);
088    }
089
090    /**
091     * Creates a new metadata with the specified scope and template.
092     *
093     * @param scope    the scope of the metadata.
094     * @param template the template of the metadata.
095     */
096    public Metadata(String scope, String template) {
097        this.values = new JsonObject()
098            .add("$scope", scope)
099            .add("$template", template);
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 getStringOrNull("/$id");
144    }
145
146    /**
147     * Returns the metadata type.
148     *
149     * @return the metadata type.
150     */
151    public String getTypeName() {
152        return getStringOrNull("/$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 getStringOrNull("/$parent");
162    }
163
164    /**
165     * Returns the scope.
166     * Can throw {@link NullPointerException} is value if not present.
167     *
168     * @return the scope.
169     */
170    public String getScope() {
171        return getStringOrNull("/$scope");
172    }
173
174    /**
175     * Returns the template name.
176     * Can throw {@link NullPointerException} is value if not present.
177     *
178     * @return the template name.
179     */
180    public String getTemplateName() {
181        return getStringOrNull("/$template");
182    }
183
184    /**
185     * Adds a new metadata value.
186     *
187     * @param path  the path that designates the key. Must be prefixed with a "/".
188     * @param value the value.
189     * @return this metadata object.
190     */
191    public Metadata add(String path, String value) {
192        this.values.add(this.pathToProperty(path), value);
193        this.addOp("add", path, value);
194        return this;
195    }
196
197    /**
198     * Adds a new metadata value.
199     *
200     * @param path  the path that designates the key. Must be prefixed with a "/".
201     * @param value the value.
202     * @return this metadata object.
203     */
204    public Metadata add(String path, double value) {
205        this.values.add(this.pathToProperty(path), value);
206        this.addOp("add", path, value);
207        return this;
208    }
209
210    /**
211     * Adds a new metadata value of array type.
212     *
213     * @param path   the path to the field.
214     * @param values the collection of values.
215     * @return the metadata object for chaining.
216     */
217    public Metadata add(String path, List<String> values) {
218        JsonArray arr = new JsonArray();
219        for (String value : values) {
220            arr.add(value);
221        }
222        this.values.add(this.pathToProperty(path), arr);
223        this.addOp("add", path, arr);
224        return this;
225    }
226
227    /**
228     * Replaces an existing metadata value.
229     *
230     * @param path  the path that designates the key. Must be prefixed with a "/".
231     * @param value the value.
232     * @return this metadata object.
233     */
234    public Metadata replace(String path, String value) {
235        this.values.set(this.pathToProperty(path), value);
236        this.addOp("replace", path, value);
237        return this;
238    }
239
240    /**
241     * Replaces an existing metadata value.
242     *
243     * @param path  the path that designates the key. Must be prefixed with a "/".
244     * @param value the value.
245     * @return this metadata object.
246     */
247    public Metadata replace(String path, float value) {
248        this.values.set(this.pathToProperty(path), value);
249        this.addOp("replace", path, value);
250        return this;
251    }
252
253    /**
254     * Replaces an existing metadata value.
255     *
256     * @param path  the path that designates the key. Must be prefixed with a "/".
257     * @param value the value.
258     * @return this metadata object.
259     */
260    public Metadata replace(String path, double value) {
261        this.values.set(this.pathToProperty(path), value);
262        this.addOp("replace", path, value);
263        return this;
264    }
265
266    /**
267     * Replaces an existing metadata value of array type.
268     *
269     * @param path   the path that designates the key. Must be prefixed with a "/".
270     * @param values the collection of values.
271     * @return the metadata object.
272     */
273    public Metadata replace(String path, List<String> values) {
274        JsonArray arr = new JsonArray();
275        for (String value : values) {
276            arr.add(value);
277        }
278        this.values.add(this.pathToProperty(path), arr);
279        this.addOp("replace", path, arr);
280        return this;
281    }
282
283    /**
284     * Removes an existing metadata value.
285     *
286     * @param path the path that designates the key. Must be prefixed with a "/".
287     * @return this metadata object.
288     */
289    public Metadata remove(String path) {
290        this.values.remove(this.pathToProperty(path));
291        this.addOp("remove", path, (String) null);
292        return this;
293    }
294
295    /**
296     * Tests that a property has the expected value.
297     *
298     * @param path  the path that designates the key. Must be prefixed with a "/".
299     * @param value the expected value.
300     * @return this metadata object.
301     */
302    public Metadata test(String path, String value) {
303        this.addOp("test", path, value);
304        return this;
305    }
306
307    /**
308     * Tests that a list of properties has the expected value.
309     * The values passed in will have to be an exact match with no extra elements.
310     *
311     * @param path   the path that designates the key. Must be prefixed with a "/".
312     * @param values the list of expected values.
313     * @return this metadata object.
314     */
315    public Metadata test(String path, List<String> values) {
316        JsonArray arr = new JsonArray();
317        for (String value : values) {
318            arr.add(value);
319        }
320        this.addOp("test", path, arr);
321        return this;
322    }
323
324    /**
325     * Returns a value, regardless of type.
326     *
327     * @param path the path that designates the key. Must be prefixed with a "/".
328     * @return the metadata property value as an indeterminate JSON type.
329     */
330    public JsonValue getValue(String path) {
331        return this.values.get(this.pathToProperty(path));
332    }
333
334    /**
335     * Get a value from a string or enum metadata field.
336     * Can throw {@link NullPointerException} is value if not present.
337     *
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     * Can throw {@link NullPointerException} is value if not present.
348     *
349     * @param path the key path in the metadata object.  Must be prefixed with a "/".
350     * @return the metadata value as a floating point number.
351     */
352    public double getDouble(String path) {
353        return this.getValue(path).asDouble();
354    }
355
356    /**
357     * Get a value from a date metadata field.
358     * Can throw {@link NullPointerException} is value if not present.
359     *
360     * @param path the key path in the metadata object.  Must be prefixed with a "/".
361     * @return the metadata value as a Date.
362     * @throws ParseException when the value cannot be parsed as a valid date
363     */
364    public Date getDate(String path) throws ParseException {
365        return BoxDateFormat.parse(this.getValue(path).asString());
366    }
367
368    /**
369     * Get a value from a multiselect metadata field.
370     *
371     * @param path the key path in the metadata object.  Must be prefixed with a "/".
372     * @return the list of values set in the field.
373     */
374    public List<String> getMultiSelect(String path) {
375        List<String> values = new ArrayList<>();
376        for (JsonValue val : this.getValue(path).asArray()) {
377            values.add(val.asString());
378        }
379
380        return values;
381    }
382
383    /**
384     * Returns a list of metadata property paths.
385     *
386     * @return the list of metdata property paths.
387     */
388    public List<String> getPropertyPaths() {
389        List<String> result = new ArrayList<>();
390
391        for (String property : this.values.names()) {
392            if (!property.startsWith("$")) {
393                result.add(this.propertyToPath(property));
394            }
395        }
396
397        return result;
398    }
399
400    /**
401     * Returns the JSON patch string with all operations.
402     *
403     * @return the JSON patch string.
404     */
405    public String getPatch() {
406        if (this.operations == null) {
407            return "[]";
408        }
409        return this.operations.toString();
410    }
411
412    /**
413     * Returns an array of operations on metadata.
414     *
415     * @return a JSON array of operations.
416     */
417    public JsonArray getOperations() {
418        return this.operations;
419    }
420
421    /**
422     * Returns the JSON representation of this metadata.
423     *
424     * @return the JSON representation of this metadata.
425     */
426    @Override
427    public String toString() {
428        return this.values.toString();
429    }
430
431    /**
432     * Converts a JSON patch path to a JSON property name.
433     * Currently the metadata API only supports flat maps.
434     *
435     * @param path the path that designates the key.  Must be prefixed with a "/".
436     * @return the JSON property name.
437     */
438    private String pathToProperty(String path) {
439        if (path == null || !path.startsWith("/")) {
440            throw new IllegalArgumentException("Path must be prefixed with a \"/\".");
441        }
442        return path.substring(1);
443    }
444
445    /**
446     * Converts a JSON property name to a JSON patch path.
447     *
448     * @param property the JSON property name.
449     * @return the path that designates the key.
450     */
451    private String propertyToPath(String property) {
452        if (property == null) {
453            throw new IllegalArgumentException("Property must not be null.");
454        }
455        return "/" + property;
456    }
457
458    /**
459     * Adds a patch operation.
460     *
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     *
479     * @param op    the operation type. Must be add, replace, remove, or test.
480     * @param path  the path that designates the key. Must be prefixed with a "/".
481     * @param value the value to be set.
482     */
483    private void addOp(String op, String path, double value) {
484        if (this.operations == null) {
485            this.operations = new JsonArray();
486        }
487
488        this.operations.add(new JsonObject()
489            .add("op", op)
490            .add("path", path)
491            .add("value", value));
492    }
493
494    /**
495     * Adds a new patch operation for array values.
496     *
497     * @param op     the operation type. Must be add, replace, remove, or test.
498     * @param path   the path that designates the key. Must be prefixed with a "/".
499     * @param values the array of values to be set.
500     */
501    private void addOp(String op, String path, JsonArray values) {
502
503        if (this.operations == null) {
504            this.operations = new JsonArray();
505        }
506
507        this.operations.add(new JsonObject()
508            .add("op", op)
509            .add("path", path)
510            .add("value", values));
511    }
512
513    private String getStringOrNull(String path) {
514        return Optional.ofNullable(this.getValue(path)).map(JsonValue::asString).orElse(null);
515    }
516}