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