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     * Replaces an existing metadata value.
168     * @param path the path that designates the key. Must be prefixed with a "/".
169     * @param value the value.
170     * @return this metadata object.
171     */
172    public Metadata replace(String path, String value) {
173        this.values.set(this.pathToProperty(path), value);
174        this.addOp("replace", path, value);
175        return this;
176    }
177
178    /**
179     * Replaces an existing metadata value.
180     * @param path the path that designates the key. Must be prefixed with a "/".
181     * @param value the value.
182     * @return this metadata object.
183     */
184    public Metadata replace(String path, float value) {
185        this.values.set(this.pathToProperty(path), value);
186        this.addOp("replace", path, value);
187        return this;
188    }
189
190    /**
191     * Removes an existing metadata value.
192     * @param path the path that designates the key. Must be prefixed with a "/".
193     * @return this metadata object.
194     */
195    public Metadata remove(String path) {
196        this.values.remove(this.pathToProperty(path));
197        this.addOp("remove", path, null);
198        return this;
199    }
200
201    /**
202     * Tests that a property has the expected value.
203     * @param path the path that designates the key. Must be prefixed with a "/".
204     * @param value the expected value.
205     * @return this metadata object.
206     */
207    public Metadata test(String path, String value) {
208        this.addOp("test", path, value);
209        return this;
210    }
211
212    /**
213     * Returns a value.
214     * @param path the path that designates the key. Must be prefixed with a "/".
215     * @return the metadata property value.
216     * @deprecated Metadata#get() does not handle all possible metadata types; use Metadata#getValue() instead
217     */
218    @Deprecated
219    public String get(String path) {
220        final JsonValue value = this.values.get(this.pathToProperty(path));
221        if (value == null) {
222            return null;
223        }
224        if (!value.isString()) {
225            return value.toString();
226        }
227        return value.asString();
228    }
229
230    /**
231     * Returns a value, regardless of type.
232     * @param path the path that designates the key. Must be prefixed with a "/".
233     * @return the metadata property value as an indeterminate JSON type.
234     */
235    public JsonValue getValue(String path) {
236        return this.values.get(this.pathToProperty(path));
237    }
238
239    /**
240     * Get a value from a string or enum metadata field.
241     * @param path the key path in the metadata object.  Must be prefixed with a "/".
242     * @return the metadata value as a string.
243     */
244    public String getString(String path) {
245        return this.getValue(path).asString();
246    }
247
248    /**
249     * Get a value from a float metadata field.
250     * @param path the key path in the metadata object.  Must be prefixed with a "/".
251     * @return the metadata value as a floating point number.
252     */
253    public double getFloat(String path) {
254        // @NOTE(mwiller) 2018-02-05: JS number are all 64-bit floating point, so double is the correct type to use here
255        return this.getValue(path).asDouble();
256    }
257
258    /**
259     * Get a value from a date metadata field.
260     * @param path the key path in the metadata object.  Must be prefixed with a "/".
261     * @return the metadata value as a Date.
262     * @throws ParseException when the value cannot be parsed as a valid date
263     */
264    public Date getDate(String path) throws ParseException {
265        return BoxDateFormat.parse(this.getValue(path).asString());
266    }
267
268    /**
269     * Returns a list of metadata property paths.
270     * @return the list of metdata property paths.
271     */
272    public List<String> getPropertyPaths() {
273        List<String> result = new ArrayList<String>();
274
275        for (String property : this.values.names()) {
276            if (!property.startsWith("$")) {
277                result.add(this.propertyToPath(property));
278            }
279        }
280
281        return result;
282    }
283
284    /**
285     * Returns the JSON patch string with all operations.
286     * @return the JSON patch string.
287     */
288    public String getPatch() {
289        if (this.operations == null) {
290            return "[]";
291        }
292        return this.operations.toString();
293    }
294
295    /**
296     * Returns the JSON representation of this metadata.
297     * @return the JSON representation of this metadata.
298     */
299    @Override
300    public String toString() {
301        return this.values.toString();
302    }
303
304    /**
305     * Converts a JSON patch path to a JSON property name.
306     * Currently the metadata API only supports flat maps.
307     * @param path the path that designates the key.  Must be prefixed with a "/".
308     * @return the JSON property name.
309     */
310    private String pathToProperty(String path) {
311        if (path == null || !path.startsWith("/")) {
312            throw new IllegalArgumentException("Path must be prefixed with a \"/\".");
313        }
314        return path.substring(1);
315    }
316
317    /**
318     * Converts a JSON property name to a JSON patch path.
319     * @param property the JSON property name.
320     * @return the path that designates the key.
321     */
322    private String propertyToPath(String property) {
323        if (property == null) {
324            throw new IllegalArgumentException("Property must not be null.");
325        }
326        return "/" + property;
327    }
328
329    /**
330     * Adds a patch operation.
331     * @param op the operation type. Must be add, replace, remove, or test.
332     * @param path the path that designates the key. Must be prefixed with a "/".
333     * @param value the value to be set.
334     */
335    private void addOp(String op, String path, String value) {
336        if (this.operations == null) {
337            this.operations = new JsonArray();
338        }
339
340        this.operations.add(new JsonObject()
341                .add("op", op)
342                .add("path", path)
343                .add("value", value));
344    }
345
346    /**
347     * Adds a patch operation.
348     * @param op the operation type. Must be add, replace, remove, or test.
349     * @param path the path that designates the key. Must be prefixed with a "/".
350     * @param value the value to be set.
351     */
352    private void addOp(String op, String path, float value) {
353        if (this.operations == null) {
354            this.operations = new JsonArray();
355        }
356
357        this.operations.add(new JsonObject()
358                .add("op", op)
359                .add("path", path)
360                .add("value", value));
361    }
362
363    static String scopeBasedOnType(String typeName) {
364        String scope;
365        if (typeName.equals(DEFAULT_METADATA_TYPE)) {
366            scope = GLOBAL_METADATA_SCOPE;
367        } else {
368            scope = ENTERPRISE_METADATA_SCOPE;
369        }
370        return scope;
371    }
372}