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