001package com.box.sdk;
002
003import java.util.ArrayList;
004import java.util.List;
005
006import com.eclipsesource.json.JsonArray;
007import com.eclipsesource.json.JsonObject;
008import com.eclipsesource.json.JsonValue;
009
010/**
011 * The Metadata class represents one type instance of Box metadata.
012 *
013 * Learn more about Box metadata:
014 * https://developers.box.com/metadata-api/
015 */
016public class Metadata {
017
018    /**
019     * Specifies the name of the default "properties" metadata template.
020     */
021    public static final String DEFAULT_METADATA_TYPE = "properties";
022
023    /**
024     * Specifies the "global" metadata scope.
025     */
026    public static final String GLOBAL_METADATA_SCOPE = "global";
027
028    /**
029     * Specifies the "enterprise" metadata scope.
030     */
031    public static final String ENTERPRISE_METADATA_SCOPE = "enterprise";
032
033    /**
034     * The default limit of entries per response.
035     */
036    public static final int DEFAULT_LIMIT = 100;
037
038    /**
039     * URL template for all metadata associated with item.
040     */
041    public static final URLTemplate GET_ALL_METADATA_URL_TEMPLATE = new URLTemplate("/metadata");
042
043    /**
044     * Values contained by the metadata object.
045     */
046    private final JsonObject values;
047
048    /**
049     * Operations to be applied to the metadata object.
050     */
051    private JsonArray operations;
052
053    /**
054     * Creates an empty metadata.
055     */
056    public Metadata() {
057        this.values = new JsonObject();
058    }
059
060    /**
061     * Creates a new metadata.
062     * @param values the initial metadata values.
063     */
064    public Metadata(JsonObject values) {
065        this.values = values;
066    }
067
068    /**
069     * Creates a copy of another metadata.
070     * @param other the other metadata object to copy.
071     */
072    public Metadata(Metadata other) {
073        this.values = new JsonObject(other.values);
074    }
075
076    /**
077     * Used to retrieve all metadata associated with the item.
078     * @param item item to get metadata for.
079     * @param fields the optional fields to retrieve.
080     * @return An iterable of metadata instances associated with the item.
081     */
082    public static Iterable<Metadata> getAllMetadata(BoxItem item, String ... fields) {
083        QueryStringBuilder builder = new QueryStringBuilder();
084        if (fields.length > 0) {
085            builder.appendParam("fields", fields);
086        }
087        return new BoxResourceIterable<Metadata>(
088                item.getAPI(),
089                GET_ALL_METADATA_URL_TEMPLATE.buildWithQuery(item.getItemURL().toString(), builder.toString()),
090                DEFAULT_LIMIT) {
091
092            @Override
093            protected Metadata factory(JsonObject jsonObject) {
094                return new Metadata(jsonObject);
095            }
096
097        };
098    }
099
100    /**
101     * Returns the 36 character UUID to identify the metadata object.
102     * @return the metadata ID.
103     */
104    public String getID() {
105        return this.get("/$id");
106    }
107
108    /**
109     * Returns the metadata type.
110     * @return the metadata type.
111     */
112    public String getTypeName() {
113        return this.get("/$type");
114    }
115
116    /**
117     * Returns the parent object ID (typically the file ID).
118     * @return the parent object ID.
119     */
120    public String getParentID() {
121        return this.get("/$parent");
122    }
123
124    /**
125     * Returns the scope.
126     * @return the scope.
127     */
128    public String getScope() {
129        return this.get("/$scope");
130    }
131
132    /**
133     * Returns the template name.
134     * @return the template name.
135     */
136    public String getTemplateName() {
137        return this.get("/$template");
138    }
139
140    /**
141     * Adds a new metadata value.
142     * @param path the path that designates the key. Must be prefixed with a "/".
143     * @param value the value.
144     * @return this metadata object.
145     */
146    public Metadata add(String path, String value) {
147        this.values.add(this.pathToProperty(path), value);
148        this.addOp("add", path, value);
149        return this;
150    }
151
152    /**
153     * Adds a new metadata value.
154     * @param path the path that designates the key. Must be prefixed with a "/".
155     * @param value the value.
156     * @return this metadata object.
157     */
158    public Metadata add(String path, float value) {
159        this.values.add(this.pathToProperty(path), value);
160        this.addOp("add", path, value);
161        return this;
162    }
163
164    /**
165     * Replaces an existing metadata value.
166     * @param path the path that designates the key. Must be prefixed with a "/".
167     * @param value the value.
168     * @return this metadata object.
169     */
170    public Metadata replace(String path, String value) {
171        this.values.set(this.pathToProperty(path), value);
172        this.addOp("replace", path, value);
173        return this;
174    }
175
176    /**
177     * Replaces an existing metadata value.
178     * @param path the path that designates the key. Must be prefixed with a "/".
179     * @param value the value.
180     * @return this metadata object.
181     */
182    public Metadata replace(String path, float value) {
183        this.values.set(this.pathToProperty(path), value);
184        this.addOp("replace", path, value);
185        return this;
186    }
187
188    /**
189     * Removes an existing metadata value.
190     * @param path the path that designates the key. Must be prefixed with a "/".
191     * @return this metadata object.
192     */
193    public Metadata remove(String path) {
194        this.values.remove(this.pathToProperty(path));
195        this.addOp("remove", path, null);
196        return this;
197    }
198
199    /**
200     * Tests that a property has the expected value.
201     * @param path the path that designates the key. Must be prefixed with a "/".
202     * @param value the expected value.
203     * @return this metadata object.
204     */
205    public Metadata test(String path, String value) {
206        this.addOp("test", path, value);
207        return this;
208    }
209
210    /**
211     * Returns a value.
212     * @param path the path that designates the key. Must be prefixed with a "/".
213     * @return the metadata property value.
214     */
215    public String get(String path) {
216        final JsonValue value = this.values.get(this.pathToProperty(path));
217        if (value == null) {
218            return null;
219        }
220        if (value.isNumber()) {
221            return value.toString();
222        }
223        return value.asString();
224    }
225
226    /**
227     * Returns a list of metadata property paths.
228     * @return the list of metdata property paths.
229     */
230    public List<String> getPropertyPaths() {
231        List<String> result = new ArrayList<String>();
232
233        for (String property : this.values.names()) {
234            if (!property.startsWith("$")) {
235                result.add(this.propertyToPath(property));
236            }
237        }
238
239        return result;
240    }
241
242    /**
243     * Returns the JSON patch string with all operations.
244     * @return the JSON patch string.
245     */
246    public String getPatch() {
247        if (this.operations == null) {
248            return "[]";
249        }
250        return this.operations.toString();
251    }
252
253    /**
254     * Returns the JSON representation of this metadata.
255     * @return the JSON representation of this metadata.
256     */
257    @Override
258    public String toString() {
259        return this.values.toString();
260    }
261
262    /**
263     * Converts a JSON patch path to a JSON property name.
264     * Currently the metadata API only supports flat maps.
265     * @param path the path that designates the key.  Must be prefixed with a "/".
266     * @return the JSON property name.
267     */
268    private String pathToProperty(String path) {
269        if (path == null || !path.startsWith("/")) {
270            throw new IllegalArgumentException("Path must be prefixed with a \"/\".");
271        }
272        return path.substring(1);
273    }
274
275    /**
276     * Converts a JSON property name to a JSON patch path.
277     * @param property the JSON property name.
278     * @return the path that designates the key.
279     */
280    private String propertyToPath(String property) {
281        if (property == null) {
282            throw new IllegalArgumentException("Property must not be null.");
283        }
284        return "/" + property;
285    }
286
287    /**
288     * Adds a patch operation.
289     * @param op the operation type. Must be add, replace, remove, or test.
290     * @param path the path that designates the key. Must be prefixed with a "/".
291     * @param value the value to be set.
292     */
293    private void addOp(String op, String path, String value) {
294        if (this.operations == null) {
295            this.operations = new JsonArray();
296        }
297
298        this.operations.add(new JsonObject()
299                .add("op", op)
300                .add("path", path)
301                .add("value", value));
302    }
303
304    /**
305     * Adds a patch operation.
306     * @param op the operation type. Must be add, replace, remove, or test.
307     * @param path the path that designates the key. Must be prefixed with a "/".
308     * @param value the value to be set.
309     */
310    private void addOp(String op, String path, float value) {
311        if (this.operations == null) {
312            this.operations = new JsonArray();
313        }
314
315        this.operations.add(new JsonObject()
316                .add("op", op)
317                .add("path", path)
318                .add("value", value));
319    }
320
321    static String scopeBasedOnType(String typeName) {
322        String scope;
323        if (typeName.equals(DEFAULT_METADATA_TYPE)) {
324            scope = GLOBAL_METADATA_SCOPE;
325        } else {
326            scope = ENTERPRISE_METADATA_SCOPE;
327        }
328        return scope;
329    }
330}