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; 010import java.util.Optional; 011 012/** 013 * The Metadata class represents one type instance of Box metadata. 014 * <p> 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 = new JsonArray(); 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 * 075 * @param values the initial metadata values. 076 */ 077 public Metadata(JsonObject values) { 078 this.values = values; 079 } 080 081 /** 082 * Creates a copy of another metadata. 083 * 084 * @param other the other metadata object to copy. 085 */ 086 public Metadata(Metadata other) { 087 this.values = new JsonObject(other.values); 088 } 089 090 /** 091 * Creates a new metadata with the specified scope and template. 092 * 093 * @param scope the scope of the metadata. 094 * @param template the template of the metadata. 095 */ 096 public Metadata(String scope, String template) { 097 this.values = new JsonObject() 098 .add("$scope", scope) 099 .add("$template", template); 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 getStringOrNull("/$id"); 144 } 145 146 /** 147 * Returns the metadata type. 148 * 149 * @return the metadata type. 150 */ 151 public String getTypeName() { 152 return getStringOrNull("/$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 getStringOrNull("/$parent"); 162 } 163 164 /** 165 * Returns the scope. 166 * Can throw {@link NullPointerException} is value if not present. 167 * 168 * @return the scope. 169 */ 170 public String getScope() { 171 return getStringOrNull("/$scope"); 172 } 173 174 /** 175 * Returns the template name. 176 * Can throw {@link NullPointerException} is value if not present. 177 * 178 * @return the template name. 179 */ 180 public String getTemplateName() { 181 return getStringOrNull("/$template"); 182 } 183 184 /** 185 * Adds a new metadata value. 186 * 187 * @param path the path that designates the key. Must be prefixed with a "/". 188 * @param value the value. 189 * @return this metadata object. 190 */ 191 public Metadata add(String path, String value) { 192 this.values.add(this.pathToProperty(path), value); 193 this.addOp("add", path, value); 194 return this; 195 } 196 197 /** 198 * Adds a new metadata value. 199 * 200 * @param path the path that designates the key. Must be prefixed with a "/". 201 * @param value the value. 202 * @return this metadata object. 203 */ 204 public Metadata add(String path, double 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 of array type. 212 * 213 * @param path the path to the field. 214 * @param values the collection of values. 215 * @return the metadata object for chaining. 216 */ 217 public Metadata add(String path, List<String> values) { 218 JsonArray arr = new JsonArray(); 219 for (String value : values) { 220 arr.add(value); 221 } 222 this.values.add(this.pathToProperty(path), arr); 223 this.addOp("add", path, arr); 224 return this; 225 } 226 227 /** 228 * Replaces an existing metadata value. 229 * 230 * @param path the path that designates the key. Must be prefixed with a "/". 231 * @param value the value. 232 * @return this metadata object. 233 */ 234 public Metadata replace(String path, String value) { 235 this.values.set(this.pathToProperty(path), value); 236 this.addOp("replace", path, value); 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, float 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, double 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 of array type. 268 * 269 * @param path the path that designates the key. Must be prefixed with a "/". 270 * @param values the collection of values. 271 * @return the metadata object. 272 */ 273 public Metadata replace(String path, List<String> values) { 274 JsonArray arr = new JsonArray(); 275 for (String value : values) { 276 arr.add(value); 277 } 278 this.values.add(this.pathToProperty(path), arr); 279 this.addOp("replace", path, arr); 280 return this; 281 } 282 283 /** 284 * Removes an existing metadata value. 285 * 286 * @param path the path that designates the key. Must be prefixed with a "/". 287 * @return this metadata object. 288 */ 289 public Metadata remove(String path) { 290 this.values.remove(this.pathToProperty(path)); 291 this.addOp("remove", path, (String) null); 292 return this; 293 } 294 295 /** 296 * Tests that a property has the expected value. 297 * 298 * @param path the path that designates the key. Must be prefixed with a "/". 299 * @param value the expected value. 300 * @return this metadata object. 301 */ 302 public Metadata test(String path, String value) { 303 this.addOp("test", path, value); 304 return this; 305 } 306 307 /** 308 * Tests that a list of properties has the expected value. 309 * The values passed in will have to be an exact match with no extra elements. 310 * 311 * @param path the path that designates the key. Must be prefixed with a "/". 312 * @param values the list of expected values. 313 * @return this metadata object. 314 */ 315 public Metadata test(String path, List<String> values) { 316 JsonArray arr = new JsonArray(); 317 for (String value : values) { 318 arr.add(value); 319 } 320 this.addOp("test", path, arr); 321 return this; 322 } 323 324 /** 325 * Returns a value, regardless of type. 326 * 327 * @param path the path that designates the key. Must be prefixed with a "/". 328 * @return the metadata property value as an indeterminate JSON type. 329 */ 330 public JsonValue getValue(String path) { 331 return this.values.get(this.pathToProperty(path)); 332 } 333 334 /** 335 * Get a value from a string or enum metadata field. 336 * Can throw {@link NullPointerException} is value if not present. 337 * 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 * Can throw {@link NullPointerException} is value if not present. 348 * 349 * @param path the key path in the metadata object. Must be prefixed with a "/". 350 * @return the metadata value as a floating point number. 351 */ 352 public double getDouble(String path) { 353 return this.getValue(path).asDouble(); 354 } 355 356 /** 357 * Get a value from a date metadata field. 358 * Can throw {@link NullPointerException} is value if not present. 359 * 360 * @param path the key path in the metadata object. Must be prefixed with a "/". 361 * @return the metadata value as a Date. 362 * @throws ParseException when the value cannot be parsed as a valid date 363 */ 364 public Date getDate(String path) throws ParseException { 365 return BoxDateFormat.parse(this.getValue(path).asString()); 366 } 367 368 /** 369 * Get a value from a multiselect metadata field. 370 * 371 * @param path the key path in the metadata object. Must be prefixed with a "/". 372 * @return the list of values set in the field. 373 */ 374 public List<String> getMultiSelect(String path) { 375 List<String> values = new ArrayList<>(); 376 for (JsonValue val : this.getValue(path).asArray()) { 377 values.add(val.asString()); 378 } 379 380 return values; 381 } 382 383 /** 384 * Returns a list of metadata property paths. 385 * 386 * @return the list of metdata property paths. 387 */ 388 public List<String> getPropertyPaths() { 389 List<String> result = new ArrayList<>(); 390 391 for (String property : this.values.names()) { 392 if (!property.startsWith("$")) { 393 result.add(this.propertyToPath(property)); 394 } 395 } 396 397 return result; 398 } 399 400 /** 401 * Returns the JSON patch string with all operations. 402 * 403 * @return the JSON patch string. 404 */ 405 public String getPatch() { 406 if (this.operations == null) { 407 return "[]"; 408 } 409 return this.operations.toString(); 410 } 411 412 /** 413 * Returns an array of operations on metadata. 414 * 415 * @return a JSON array of operations. 416 */ 417 public JsonArray getOperations() { 418 return this.operations; 419 } 420 421 /** 422 * Returns the JSON representation of this metadata. 423 * 424 * @return the JSON representation of this metadata. 425 */ 426 @Override 427 public String toString() { 428 return this.values.toString(); 429 } 430 431 /** 432 * Converts a JSON patch path to a JSON property name. 433 * Currently the metadata API only supports flat maps. 434 * 435 * @param path the path that designates the key. Must be prefixed with a "/". 436 * @return the JSON property name. 437 */ 438 private String pathToProperty(String path) { 439 if (path == null || !path.startsWith("/")) { 440 throw new IllegalArgumentException("Path must be prefixed with a \"/\"."); 441 } 442 return path.substring(1); 443 } 444 445 /** 446 * Converts a JSON property name to a JSON patch path. 447 * 448 * @param property the JSON property name. 449 * @return the path that designates the key. 450 */ 451 private String propertyToPath(String property) { 452 if (property == null) { 453 throw new IllegalArgumentException("Property must not be null."); 454 } 455 return "/" + property; 456 } 457 458 /** 459 * Adds a patch operation. 460 * 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 * 479 * @param op the operation type. Must be add, replace, remove, or test. 480 * @param path the path that designates the key. Must be prefixed with a "/". 481 * @param value the value to be set. 482 */ 483 private void addOp(String op, String path, double value) { 484 if (this.operations == null) { 485 this.operations = new JsonArray(); 486 } 487 488 this.operations.add(new JsonObject() 489 .add("op", op) 490 .add("path", path) 491 .add("value", value)); 492 } 493 494 /** 495 * Adds a new patch operation for array values. 496 * 497 * @param op the operation type. Must be add, replace, remove, or test. 498 * @param path the path that designates the key. Must be prefixed with a "/". 499 * @param values the array of values to be set. 500 */ 501 private void addOp(String op, String path, JsonArray values) { 502 503 if (this.operations == null) { 504 this.operations = new JsonArray(); 505 } 506 507 this.operations.add(new JsonObject() 508 .add("op", op) 509 .add("path", path) 510 .add("value", values)); 511 } 512 513 private String getStringOrNull(String path) { 514 return Optional.ofNullable(this.getValue(path)).map(JsonValue::asString).orElse(null); 515 } 516}