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     * The default limit of entries per response.
037     */
038    public static final int DEFAULT_LIMIT = 100;
039
040    /**
041     * URL template for all metadata associated with item.
042     */
043    public static final URLTemplate GET_ALL_METADATA_URL_TEMPLATE = new URLTemplate("/metadata");
044
045    /**
046     * Values contained by the metadata object.
047     */
048    private final JsonObject values;
049
050    /**
051     * Operations to be applied to the metadata object.
052     */
053    private JsonArray operations;
054
055    /**
056     * Creates an empty metadata.
057     */
058    public Metadata() {
059        this.values = new JsonObject();
060    }
061
062    /**
063     * Creates a new metadata.
064     * @param values the initial metadata values.
065     */
066    public Metadata(JsonObject values) {
067        this.values = values;
068    }
069
070    /**
071     * Creates a copy of another metadata.
072     * @param other the other metadata object to copy.
073     */
074    public Metadata(Metadata other) {
075        this.values = new JsonObject(other.values);
076    }
077
078    /**
079     * Used to retrieve all metadata associated with the item.
080     * @param item item to get metadata for.
081     * @param fields the optional fields to retrieve.
082     * @return An iterable of metadata instances associated with the item.
083     */
084    public static Iterable<Metadata> getAllMetadata(BoxItem item, String ... fields) {
085        QueryStringBuilder builder = new QueryStringBuilder();
086        if (fields.length > 0) {
087            builder.appendParam("fields", fields);
088        }
089        return new BoxResourceIterable<Metadata>(
090                item.getAPI(),
091                GET_ALL_METADATA_URL_TEMPLATE.buildWithQuery(item.getItemURL().toString(), builder.toString()),
092                DEFAULT_LIMIT) {
093
094            @Override
095            protected Metadata factory(JsonObject jsonObject) {
096                return new Metadata(jsonObject);
097            }
098
099        };
100    }
101
102    /**
103     * Returns the 36 character UUID to identify the metadata object.
104     * @return the metadata ID.
105     */
106    public String getID() {
107        return this.get("/$id");
108    }
109
110    /**
111     * Returns the metadata type.
112     * @return the metadata type.
113     */
114    public String getTypeName() {
115        return this.get("/$type");
116    }
117
118    /**
119     * Returns the parent object ID (typically the file ID).
120     * @return the parent object ID.
121     */
122    public String getParentID() {
123        return this.get("/$parent");
124    }
125
126    /**
127     * Returns the scope.
128     * @return the scope.
129     */
130    public String getScope() {
131        return this.get("/$scope");
132    }
133
134    /**
135     * Returns the template name.
136     * @return the template name.
137     */
138    public String getTemplateName() {
139        return this.get("/$template");
140    }
141
142    /**
143     * Adds a new metadata value.
144     * @param path the path that designates the key. Must be prefixed with a "/".
145     * @param value the value.
146     * @return this metadata object.
147     */
148    public Metadata add(String path, String value) {
149        this.values.add(this.pathToProperty(path), value);
150        this.addOp("add", path, value);
151        return this;
152    }
153
154    /**
155     * Adds a new metadata value.
156     * @param path the path that designates the key. Must be prefixed with a "/".
157     * @param value the value.
158     * @return this metadata object.
159     */
160    public Metadata add(String path, float value) {
161        this.values.add(this.pathToProperty(path), value);
162        this.addOp("add", path, value);
163        return this;
164    }
165
166    /**
167     * Adds a new metadata value of array type.
168     * @param path the path to the field.
169     * @param values the collection of values.
170     * @return the metadata object for chaining.
171     */
172    public Metadata add(String path, List<String> values) {
173        JsonArray arr = new JsonArray();
174        for (String value : values) {
175            arr.add(value);
176        }
177        this.values.add(this.pathToProperty(path), arr);
178        this.addOp("add", path, arr);
179        return this;
180    }
181
182    /**
183     * Replaces an existing metadata value.
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 replace(String path, String value) {
189        this.values.set(this.pathToProperty(path), value);
190        this.addOp("replace", path, value);
191        return this;
192    }
193
194    /**
195     * Replaces an existing metadata value.
196     * @param path the path that designates the key. Must be prefixed with a "/".
197     * @param value the value.
198     * @return this metadata object.
199     */
200    public Metadata replace(String path, float value) {
201        this.values.set(this.pathToProperty(path), value);
202        this.addOp("replace", path, value);
203        return this;
204    }
205
206    /**
207     * Removes an existing metadata value.
208     * @param path the path that designates the key. Must be prefixed with a "/".
209     * @return this metadata object.
210     */
211    public Metadata remove(String path) {
212        this.values.remove(this.pathToProperty(path));
213        this.addOp("remove", path, (String) null);
214        return this;
215    }
216
217    /**
218     * Tests that a property has the expected value.
219     * @param path the path that designates the key. Must be prefixed with a "/".
220     * @param value the expected value.
221     * @return this metadata object.
222     */
223    public Metadata test(String path, String value) {
224        this.addOp("test", path, value);
225        return this;
226    }
227
228    /**
229     * Tests that a list of properties has the expected value.
230     * The values passed in will have to be an exact match with no extra elements.
231     * @param path      the path that designates the key. Must be prefixed with a "/".
232     * @param values    the list of expected values.
233     * @return          this metadata object.
234     */
235    public Metadata test(String path, List<String> values) {
236        JsonArray arr = new JsonArray();
237        for (String value : values) {
238            arr.add(value);
239        }
240        this.addOp("test", path, arr);
241        return this;
242    }
243
244    /**
245     * Returns a value.
246     * @param path the path that designates the key. Must be prefixed with a "/".
247     * @return the metadata property value.
248     * @deprecated Metadata#get() does not handle all possible metadata types; use Metadata#getValue() instead
249     */
250    @Deprecated
251    public String get(String path) {
252        final JsonValue value = this.values.get(this.pathToProperty(path));
253        if (value == null) {
254            return null;
255        }
256        if (!value.isString()) {
257            return value.toString();
258        }
259        return value.asString();
260    }
261
262    /**
263     * Returns a value, regardless of type.
264     * @param path the path that designates the key. Must be prefixed with a "/".
265     * @return the metadata property value as an indeterminate JSON type.
266     */
267    public JsonValue getValue(String path) {
268        return this.values.get(this.pathToProperty(path));
269    }
270
271    /**
272     * Get a value from a string or enum metadata field.
273     * @param path the key path in the metadata object.  Must be prefixed with a "/".
274     * @return the metadata value as a string.
275     */
276    public String getString(String path) {
277        return this.getValue(path).asString();
278    }
279
280    /**
281     * Get a value from a float metadata field.
282     * @param path the key path in the metadata object.  Must be prefixed with a "/".
283     * @return the metadata value as a floating point number.
284     */
285    public double getFloat(String path) {
286        // @NOTE(mwiller) 2018-02-05: JS number are all 64-bit floating point, so double is the correct type to use here
287        return this.getValue(path).asDouble();
288    }
289
290    /**
291     * Get a value from a date metadata field.
292     * @param path the key path in the metadata object.  Must be prefixed with a "/".
293     * @return the metadata value as a Date.
294     * @throws ParseException when the value cannot be parsed as a valid date
295     */
296    public Date getDate(String path) throws ParseException {
297        return BoxDateFormat.parse(this.getValue(path).asString());
298    }
299
300    /**
301     * Get a value from a multiselect metadata field.
302     * @param path the key path in the metadata object.  Must be prefixed with a "/".
303     * @return the list of values set in the field.
304     */
305    public List<String> getMultiSelect(String path) {
306        List<String> values = new ArrayList<String>();
307        for (JsonValue val : this.getValue(path).asArray()) {
308            values.add(val.asString());
309        }
310
311        return values;
312    }
313
314    /**
315     * Returns a list of metadata property paths.
316     * @return the list of metdata property paths.
317     */
318    public List<String> getPropertyPaths() {
319        List<String> result = new ArrayList<String>();
320
321        for (String property : this.values.names()) {
322            if (!property.startsWith("$")) {
323                result.add(this.propertyToPath(property));
324            }
325        }
326
327        return result;
328    }
329
330    /**
331     * Returns the JSON patch string with all operations.
332     * @return the JSON patch string.
333     */
334    public String getPatch() {
335        if (this.operations == null) {
336            return "[]";
337        }
338        return this.operations.toString();
339    }
340
341    /**
342     * Returns the JSON representation of this metadata.
343     * @return the JSON representation of this metadata.
344     */
345    @Override
346    public String toString() {
347        return this.values.toString();
348    }
349
350    /**
351     * Converts a JSON patch path to a JSON property name.
352     * Currently the metadata API only supports flat maps.
353     * @param path the path that designates the key.  Must be prefixed with a "/".
354     * @return the JSON property name.
355     */
356    private String pathToProperty(String path) {
357        if (path == null || !path.startsWith("/")) {
358            throw new IllegalArgumentException("Path must be prefixed with a \"/\".");
359        }
360        return path.substring(1);
361    }
362
363    /**
364     * Converts a JSON property name to a JSON patch path.
365     * @param property the JSON property name.
366     * @return the path that designates the key.
367     */
368    private String propertyToPath(String property) {
369        if (property == null) {
370            throw new IllegalArgumentException("Property must not be null.");
371        }
372        return "/" + property;
373    }
374
375    /**
376     * Adds a patch operation.
377     * @param op the operation type. Must be add, replace, remove, or test.
378     * @param path the path that designates the key. Must be prefixed with a "/".
379     * @param value the value to be set.
380     */
381    private void addOp(String op, String path, String value) {
382        if (this.operations == null) {
383            this.operations = new JsonArray();
384        }
385
386        this.operations.add(new JsonObject()
387                .add("op", op)
388                .add("path", path)
389                .add("value", value));
390    }
391
392    /**
393     * Adds a patch operation.
394     * @param op the operation type. Must be add, replace, remove, or test.
395     * @param path the path that designates the key. Must be prefixed with a "/".
396     * @param value the value to be set.
397     */
398    private void addOp(String op, String path, float value) {
399        if (this.operations == null) {
400            this.operations = new JsonArray();
401        }
402
403        this.operations.add(new JsonObject()
404                .add("op", op)
405                .add("path", path)
406                .add("value", value));
407    }
408
409    /**
410     * Adds a new patch operation for array values.
411     * @param op the operation type. Must be add, replace, remove, or test.
412     * @param path the path that designates the key. Must be prefixed with a "/".
413     * @param values the array of values to be set.
414     */
415    private void addOp(String op, String path, JsonArray values) {
416
417        if (this.operations == null) {
418            this.operations = new JsonArray();
419        }
420
421        this.operations.add(new JsonObject()
422                .add("op", op)
423                .add("path", path)
424                .add("value", values));
425    }
426
427    static String scopeBasedOnType(String typeName) {
428        String scope;
429        if (typeName.equals(DEFAULT_METADATA_TYPE)) {
430            scope = GLOBAL_METADATA_SCOPE;
431        } else {
432            scope = ENTERPRISE_METADATA_SCOPE;
433        }
434        return scope;
435    }
436}