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