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