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}