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 */ 182 public Metadata add(String path, float value) { 183 this.values.add(this.pathToProperty(path), value); 184 this.addOp("add", path, value); 185 return this; 186 } 187 188 /** 189 * Adds a new metadata value of array type. 190 * @param path the path to the field. 191 * @param values the collection of values. 192 * @return the metadata object for chaining. 193 */ 194 public Metadata add(String path, List<String> values) { 195 JsonArray arr = new JsonArray(); 196 for (String value : values) { 197 arr.add(value); 198 } 199 this.values.add(this.pathToProperty(path), arr); 200 this.addOp("add", path, arr); 201 return this; 202 } 203 204 /** 205 * Replaces an existing metadata value. 206 * @param path the path that designates the key. Must be prefixed with a "/". 207 * @param value the value. 208 * @return this metadata object. 209 */ 210 public Metadata replace(String path, String value) { 211 this.values.set(this.pathToProperty(path), value); 212 this.addOp("replace", path, value); 213 return this; 214 } 215 216 /** 217 * Replaces an existing metadata value. 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 of array type. 230 * @param path the path that designates the key. Must be prefixed with a "/". 231 * @param values the collection of values. 232 * @return the metadata object. 233 */ 234 public Metadata replace(String path, List<String> values) { 235 JsonArray arr = new JsonArray(); 236 for (String value : values) { 237 arr.add(value); 238 } 239 this.values.add(this.pathToProperty(path), arr); 240 this.addOp("replace", path, arr); 241 return this; 242 } 243 244 /** 245 * Removes an existing metadata value. 246 * @param path the path that designates the key. Must be prefixed with a "/". 247 * @return this metadata object. 248 */ 249 public Metadata remove(String path) { 250 this.values.remove(this.pathToProperty(path)); 251 this.addOp("remove", path, (String) null); 252 return this; 253 } 254 255 /** 256 * Tests that a property has the expected value. 257 * @param path the path that designates the key. Must be prefixed with a "/". 258 * @param value the expected value. 259 * @return this metadata object. 260 */ 261 public Metadata test(String path, String value) { 262 this.addOp("test", path, value); 263 return this; 264 } 265 266 /** 267 * Tests that a list of properties has the expected value. 268 * The values passed in will have to be an exact match with no extra elements. 269 * @param path the path that designates the key. Must be prefixed with a "/". 270 * @param values the list of expected values. 271 * @return this metadata object. 272 */ 273 public Metadata test(String path, List<String> values) { 274 JsonArray arr = new JsonArray(); 275 for (String value : values) { 276 arr.add(value); 277 } 278 this.addOp("test", path, arr); 279 return this; 280 } 281 282 /** 283 * Returns a value. 284 * @param path the path that designates the key. Must be prefixed with a "/". 285 * @return the metadata property value. 286 * @deprecated Metadata#get() does not handle all possible metadata types; use Metadata#getValue() instead 287 */ 288 @Deprecated 289 public String get(String path) { 290 final JsonValue value = this.values.get(this.pathToProperty(path)); 291 if (value == null) { 292 return null; 293 } 294 if (!value.isString()) { 295 return value.toString(); 296 } 297 return value.asString(); 298 } 299 300 /** 301 * Returns a value, regardless of type. 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. 311 * @param path the key path in the metadata object. Must be prefixed with a "/". 312 * @return the metadata value as a string. 313 */ 314 public String getString(String path) { 315 return this.getValue(path).asString(); 316 } 317 318 /** 319 * Get a value from a float metadata field. 320 * @param path the key path in the metadata object. Must be prefixed with a "/". 321 * @return the metadata value as a floating point number. 322 */ 323 public double getFloat(String path) { 324 // @NOTE(mwiller) 2018-02-05: JS number are all 64-bit floating point, so double is the correct type to use here 325 return this.getValue(path).asDouble(); 326 } 327 328 /** 329 * Get a value from a date metadata field. 330 * @param path the key path in the metadata object. Must be prefixed with a "/". 331 * @return the metadata value as a Date. 332 * @throws ParseException when the value cannot be parsed as a valid date 333 */ 334 public Date getDate(String path) throws ParseException { 335 return BoxDateFormat.parse(this.getValue(path).asString()); 336 } 337 338 /** 339 * Get a value from a multiselect metadata field. 340 * @param path the key path in the metadata object. Must be prefixed with a "/". 341 * @return the list of values set in the field. 342 */ 343 public List<String> getMultiSelect(String path) { 344 List<String> values = new ArrayList<String>(); 345 for (JsonValue val : this.getValue(path).asArray()) { 346 values.add(val.asString()); 347 } 348 349 return values; 350 } 351 352 /** 353 * Returns a list of metadata property paths. 354 * @return the list of metdata property paths. 355 */ 356 public List<String> getPropertyPaths() { 357 List<String> result = new ArrayList<String>(); 358 359 for (String property : this.values.names()) { 360 if (!property.startsWith("$")) { 361 result.add(this.propertyToPath(property)); 362 } 363 } 364 365 return result; 366 } 367 368 /** 369 * Returns the JSON patch string with all operations. 370 * @return the JSON patch string. 371 */ 372 public String getPatch() { 373 if (this.operations == null) { 374 return "[]"; 375 } 376 return this.operations.toString(); 377 } 378 379 /** 380 * Returns an array of operations on metadata. 381 * @return a JSON array of operations. 382 */ 383 public JsonArray getOperations() { 384 return this.operations; 385 } 386 387 /** 388 * Returns the JSON representation of this metadata. 389 * @return the JSON representation of this metadata. 390 */ 391 @Override 392 public String toString() { 393 return this.values.toString(); 394 } 395 396 /** 397 * Converts a JSON patch path to a JSON property name. 398 * Currently the metadata API only supports flat maps. 399 * @param path the path that designates the key. Must be prefixed with a "/". 400 * @return the JSON property name. 401 */ 402 private String pathToProperty(String path) { 403 if (path == null || !path.startsWith("/")) { 404 throw new IllegalArgumentException("Path must be prefixed with a \"/\"."); 405 } 406 return path.substring(1); 407 } 408 409 /** 410 * Converts a JSON property name to a JSON patch path. 411 * @param property the JSON property name. 412 * @return the path that designates the key. 413 */ 414 private String propertyToPath(String property) { 415 if (property == null) { 416 throw new IllegalArgumentException("Property must not be null."); 417 } 418 return "/" + property; 419 } 420 421 /** 422 * Adds a patch operation. 423 * @param op the operation type. Must be add, replace, remove, or test. 424 * @param path the path that designates the key. Must be prefixed with a "/". 425 * @param value the value to be set. 426 */ 427 private void addOp(String op, String path, String value) { 428 if (this.operations == null) { 429 this.operations = new JsonArray(); 430 } 431 432 this.operations.add(new JsonObject() 433 .add("op", op) 434 .add("path", path) 435 .add("value", value)); 436 } 437 438 /** 439 * Adds a patch operation. 440 * @param op the operation type. Must be add, replace, remove, or test. 441 * @param path the path that designates the key. Must be prefixed with a "/". 442 * @param value the value to be set. 443 */ 444 private void addOp(String op, String path, float value) { 445 if (this.operations == null) { 446 this.operations = new JsonArray(); 447 } 448 449 this.operations.add(new JsonObject() 450 .add("op", op) 451 .add("path", path) 452 .add("value", value)); 453 } 454 455 /** 456 * Adds a new patch operation for array values. 457 * @param op the operation type. Must be add, replace, remove, or test. 458 * @param path the path that designates the key. Must be prefixed with a "/". 459 * @param values the array of values to be set. 460 */ 461 private void addOp(String op, String path, JsonArray values) { 462 463 if (this.operations == null) { 464 this.operations = new JsonArray(); 465 } 466 467 this.operations.add(new JsonObject() 468 .add("op", op) 469 .add("path", path) 470 .add("value", values)); 471 } 472 473 static String scopeBasedOnType(String typeName) { 474 String scope; 475 if (typeName.equals(DEFAULT_METADATA_TYPE)) { 476 scope = GLOBAL_METADATA_SCOPE; 477 } else { 478 scope = ENTERPRISE_METADATA_SCOPE; 479 } 480 return scope; 481 } 482}