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     * Replaces an existing 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 replace(String path, String value) {
159        this.values.set(this.pathToProperty(path), value);
160        this.addOp("replace", 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, float value) {
171        this.values.set(this.pathToProperty(path), value);
172        this.addOp("replace", path, value);
173        return this;
174    }
175
176    /**
177     * Removes an existing metadata value.
178     * @param path the path that designates the key. Must be prefixed with a "/".
179     * @return this metadata object.
180     */
181    public Metadata remove(String path) {
182        this.values.remove(this.pathToProperty(path));
183        this.addOp("remove", path, null);
184        return this;
185    }
186
187    /**
188     * Tests that a property has the expected value.
189     * @param path the path that designates the key. Must be prefixed with a "/".
190     * @param value the expected value.
191     * @return this metadata object.
192     */
193    public Metadata test(String path, String value) {
194        this.addOp("test", path, value);
195        return this;
196    }
197
198    /**
199     * Returns a value.
200     * @param path the path that designates the key. Must be prefixed with a "/".
201     * @return the metadata property value.
202     */
203    public String get(String path) {
204        final JsonValue value = this.values.get(this.pathToProperty(path));
205        if (value == null) {
206            return null;
207        }
208        if (value.isNumber()) {
209            return value.toString();
210        }
211        return value.asString();
212    }
213
214    /**
215     * Returns a list of metadata property paths.
216     * @return the list of metdata property paths.
217     */
218    public List<String> getPropertyPaths() {
219        List<String> result = new ArrayList<String>();
220
221        for (String property : this.values.names()) {
222            if (!property.startsWith("$")) {
223                result.add(this.propertyToPath(property));
224            }
225        }
226
227        return result;
228    }
229
230    /**
231     * Returns the JSON patch string with all operations.
232     * @return the JSON patch string.
233     */
234    public String getPatch() {
235        if (this.operations == null) {
236            return "[]";
237        }
238        return this.operations.toString();
239    }
240
241    /**
242     * Returns the JSON representation of this metadata.
243     * @return the JSON representation of this metadata.
244     */
245    @Override
246    public String toString() {
247        return this.values.toString();
248    }
249
250    /**
251     * Converts a JSON patch path to a JSON property name.
252     * Currently the metadata API only supports flat maps.
253     * @param path the path that designates the key.  Must be prefixed with a "/".
254     * @return the JSON property name.
255     */
256    private String pathToProperty(String path) {
257        if (path == null || !path.startsWith("/")) {
258            throw new IllegalArgumentException("Path must be prefixed with a \"/\".");
259        }
260        return path.substring(1);
261    }
262
263    /**
264     * Converts a JSON property name to a JSON patch path.
265     * @param property the JSON property name.
266     * @return the path that designates the key.
267     */
268    private String propertyToPath(String property) {
269        if (property == null) {
270            throw new IllegalArgumentException("Property must not be null.");
271        }
272        return "/" + property;
273    }
274
275    /**
276     * Adds a patch operation.
277     * @param op the operation type. Must be add, replace, remove, or test.
278     * @param path the path that designates the key. Must be prefixed with a "/".
279     * @param value the value to be set.
280     */
281    private void addOp(String op, String path, String value) {
282        if (this.operations == null) {
283            this.operations = new JsonArray();
284        }
285
286        this.operations.add(new JsonObject()
287                .add("op", op)
288                .add("path", path)
289                .add("value", value));
290    }
291
292    /**
293     * Adds a patch operation.
294     * @param op the operation type. Must be add, replace, remove, or test.
295     * @param path the path that designates the key. Must be prefixed with a "/".
296     * @param value the value to be set.
297     */
298    private void addOp(String op, String path, float value) {
299        if (this.operations == null) {
300            this.operations = new JsonArray();
301        }
302
303        this.operations.add(new JsonObject()
304                .add("op", op)
305                .add("path", path)
306                .add("value", value));
307    }
308
309    static String scopeBasedOnType(String typeName) {
310        String scope;
311        if (typeName.equals(DEFAULT_METADATA_TYPE)) {
312            scope = GLOBAL_METADATA_SCOPE;
313        } else {
314            scope = ENTERPRISE_METADATA_SCOPE;
315        }
316        return scope;
317    }
318}