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