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