001package com.box.sdk; 002 003import java.io.IOException; 004import java.io.InputStream; 005import java.io.OutputStream; 006import java.net.URL; 007import java.util.ArrayList; 008import java.util.Collection; 009import java.util.Date; 010import java.util.EnumSet; 011import java.util.List; 012 013import com.eclipsesource.json.JsonArray; 014import com.eclipsesource.json.JsonObject; 015import com.eclipsesource.json.JsonValue; 016 017/** 018 * Represents an individual file on Box. This class can be used to download a file's contents, upload new versions, and 019 * perform other common file operations (move, copy, delete, etc.). 020 */ 021public class BoxFile extends BoxItem { 022 /** 023 * An array of all possible file fields that can be requested when calling {@link #getInfo()}. 024 */ 025 public static final String[] ALL_FIELDS = {"type", "id", "sequence_id", "etag", "sha1", "name", "description", 026 "size", "path_collection", "created_at", "modified_at", "trashed_at", "purged_at", "content_created_at", 027 "content_modified_at", "created_by", "modified_by", "owned_by", "shared_link", "parent", "item_status", 028 "version_number", "comment_count", "permissions", "tags", "lock", "extension", "is_package"}; 029 030 private static final URLTemplate FILE_URL_TEMPLATE = new URLTemplate("files/%s"); 031 private static final URLTemplate CONTENT_URL_TEMPLATE = new URLTemplate("files/%s/content"); 032 private static final URLTemplate VERSIONS_URL_TEMPLATE = new URLTemplate("files/%s/versions"); 033 private static final URLTemplate COPY_URL_TEMPLATE = new URLTemplate("files/%s/copy"); 034 private static final URLTemplate ADD_COMMENT_URL_TEMPLATE = new URLTemplate("comments"); 035 private static final URLTemplate GET_COMMENTS_URL_TEMPLATE = new URLTemplate("files/%s/comments"); 036 private static final int BUFFER_SIZE = 8192; 037 038 private final URL fileURL; 039 private final URL contentURL; 040 041 /** 042 * Constructs a BoxFile for a file with a given ID. 043 * @param api the API connection to be used by the file. 044 * @param id the ID of the file. 045 */ 046 public BoxFile(BoxAPIConnection api, String id) { 047 super(api, id); 048 049 this.fileURL = FILE_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID()); 050 this.contentURL = CONTENT_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID()); 051 } 052 053 @Override 054 public BoxSharedLink createSharedLink(BoxSharedLink.Access access, Date unshareDate, 055 BoxSharedLink.Permissions permissions) { 056 057 BoxSharedLink sharedLink = new BoxSharedLink(access, unshareDate, permissions); 058 Info info = new Info(); 059 info.setSharedLink(sharedLink); 060 061 this.updateInfo(info); 062 return info.getSharedLink(); 063 } 064 065 /** 066 * Adds a comment to this file. The message can contain @mentions by using the string @[userid:username] anywhere 067 * within the message, where userid and username are the ID and username of the person being mentioned. 068 * @see <a href="https://developers.box.com/docs/#comments-add-a-comment-to-an-item">the tagged_message field 069 * for including @mentions.</a> 070 * @param message the comment's message. 071 * @return information about the newly added comment. 072 */ 073 public BoxComment.Info addComment(String message) { 074 JsonObject itemJSON = new JsonObject(); 075 itemJSON.add("type", "file"); 076 itemJSON.add("id", this.getID()); 077 078 JsonObject requestJSON = new JsonObject(); 079 requestJSON.add("item", itemJSON); 080 if (BoxComment.messageContainsMention(message)) { 081 requestJSON.add("tagged_message", message); 082 } else { 083 requestJSON.add("message", message); 084 } 085 086 URL url = ADD_COMMENT_URL_TEMPLATE.build(this.getAPI().getBaseURL()); 087 BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "POST"); 088 request.setBody(requestJSON.toString()); 089 BoxJSONResponse response = (BoxJSONResponse) request.send(); 090 JsonObject responseJSON = JsonObject.readFrom(response.getJSON()); 091 092 BoxComment addedComment = new BoxComment(this.getAPI(), responseJSON.get("id").asString()); 093 return addedComment.new Info(responseJSON); 094 } 095 096 /** 097 * Downloads the contents of this file to a given OutputStream. 098 * @param output the stream to where the file will be written. 099 */ 100 public void download(OutputStream output) { 101 this.download(output, null); 102 } 103 104 /** 105 * Downloads the contents of this file to a given OutputStream while reporting the progress to a ProgressListener. 106 * @param output the stream to where the file will be written. 107 * @param listener a listener for monitoring the download's progress. 108 */ 109 public void download(OutputStream output, ProgressListener listener) { 110 BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), this.contentURL, "GET"); 111 BoxAPIResponse response = request.send(); 112 InputStream input = response.getBody(listener); 113 114 byte[] buffer = new byte[BUFFER_SIZE]; 115 try { 116 int n = input.read(buffer); 117 while (n != -1) { 118 output.write(buffer, 0, n); 119 n = input.read(buffer); 120 } 121 } catch (IOException e) { 122 throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e); 123 } 124 125 response.disconnect(); 126 } 127 128 /** 129 * Downloads a part of this file's contents, starting at specified byte offset. 130 * @param output the stream to where the file will be written. 131 * @param offset the byte offset at which to start the download. 132 */ 133 public void downloadRange(OutputStream output, long offset) { 134 this.downloadRange(output, offset, -1); 135 } 136 137 /** 138 * Downloads a part of this file's contents, starting at rangeStart and stopping at rangeEnd. 139 * @param output the stream to where the file will be written. 140 * @param rangeStart the byte offset at which to start the download. 141 * @param rangeEnd the byte offset at which to stop the download. 142 */ 143 public void downloadRange(OutputStream output, long rangeStart, long rangeEnd) { 144 this.downloadRange(output, rangeStart, rangeEnd, null); 145 } 146 147 /** 148 * Downloads a part of this file's contents, starting at rangeStart and stopping at rangeEnd, while reporting the 149 * progress to a ProgressListener. 150 * @param output the stream to where the file will be written. 151 * @param rangeStart the byte offset at which to start the download. 152 * @param rangeEnd the byte offset at which to stop the download. 153 * @param listener a listener for monitoring the download's progress. 154 */ 155 public void downloadRange(OutputStream output, long rangeStart, long rangeEnd, ProgressListener listener) { 156 BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), this.contentURL, "GET"); 157 if (rangeEnd > 0) { 158 request.addHeader("Range", String.format("bytes=%s-%s", Long.toString(rangeStart), 159 Long.toString(rangeEnd))); 160 } else { 161 request.addHeader("Range", String.format("bytes=%s-", Long.toString(rangeStart))); 162 } 163 164 BoxAPIResponse response = request.send(); 165 InputStream input = response.getBody(listener); 166 167 byte[] buffer = new byte[BUFFER_SIZE]; 168 try { 169 int n = input.read(buffer); 170 while (n != -1) { 171 output.write(buffer, 0, n); 172 n = input.read(buffer); 173 } 174 } catch (IOException e) { 175 throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e); 176 } 177 178 response.disconnect(); 179 } 180 181 @Override 182 public BoxFile.Info copy(BoxFolder destination) { 183 return this.copy(destination, null); 184 } 185 186 @Override 187 public BoxFile.Info copy(BoxFolder destination, String newName) { 188 URL url = COPY_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID()); 189 190 JsonObject parent = new JsonObject(); 191 parent.add("id", destination.getID()); 192 193 JsonObject copyInfo = new JsonObject(); 194 copyInfo.add("parent", parent); 195 if (newName != null) { 196 copyInfo.add("name", newName); 197 } 198 199 BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "POST"); 200 request.setBody(copyInfo.toString()); 201 BoxJSONResponse response = (BoxJSONResponse) request.send(); 202 JsonObject responseJSON = JsonObject.readFrom(response.getJSON()); 203 BoxFile copiedFile = new BoxFile(this.getAPI(), responseJSON.get("id").asString()); 204 return copiedFile.new Info(responseJSON); 205 } 206 207 /** 208 * Deletes this file by moving it to the trash. 209 */ 210 public void delete() { 211 BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), this.fileURL, "DELETE"); 212 BoxAPIResponse response = request.send(); 213 response.disconnect(); 214 } 215 216 /** 217 * Renames this file. 218 * @param newName the new name of the file. 219 */ 220 public void rename(String newName) { 221 BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), this.fileURL, "PUT"); 222 223 JsonObject updateInfo = new JsonObject(); 224 updateInfo.add("name", newName); 225 226 request.setBody(updateInfo.toString()); 227 BoxAPIResponse response = request.send(); 228 response.disconnect(); 229 } 230 231 @Override 232 public BoxFile.Info getInfo() { 233 BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), this.fileURL, "GET"); 234 BoxJSONResponse response = (BoxJSONResponse) request.send(); 235 return new Info(response.getJSON()); 236 } 237 238 @Override 239 public BoxFile.Info getInfo(String... fields) { 240 String queryString = new QueryStringBuilder().appendParam("fields", fields).toString(); 241 URL url = FILE_URL_TEMPLATE.buildWithQuery(this.getAPI().getBaseURL(), queryString, this.getID()); 242 243 BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET"); 244 BoxJSONResponse response = (BoxJSONResponse) request.send(); 245 return new Info(response.getJSON()); 246 } 247 248 /** 249 * Updates the information about this file with any info fields that have been modified locally. 250 * 251 * <p>The only fields that will be updated are the ones that have been modified locally. For example, the following 252 * code won't update any information (or even send a network request) since none of the info's fields were 253 * changed:</p> 254 * 255 * <pre>BoxFile file = new File(api, id); 256 *BoxFile.Info info = file.getInfo(); 257 *file.updateInfo(info);</pre> 258 * 259 * @param info the updated info. 260 */ 261 public void updateInfo(BoxFile.Info info) { 262 URL url = FILE_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID()); 263 BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "PUT"); 264 request.setBody(info.getPendingChanges()); 265 BoxJSONResponse response = (BoxJSONResponse) request.send(); 266 JsonObject jsonObject = JsonObject.readFrom(response.getJSON()); 267 info.update(jsonObject); 268 } 269 270 /** 271 * Gets any previous versions of this file. Note that only users with premium accounts will be able to retrieve 272 * previous versions of their files. 273 * @return a list of previous file versions. 274 */ 275 public Collection<BoxFileVersion> getVersions() { 276 URL url = VERSIONS_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID()); 277 BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET"); 278 BoxJSONResponse response = (BoxJSONResponse) request.send(); 279 280 JsonObject jsonObject = JsonObject.readFrom(response.getJSON()); 281 JsonArray entries = jsonObject.get("entries").asArray(); 282 Collection<BoxFileVersion> versions = new ArrayList<BoxFileVersion>(); 283 for (JsonValue entry : entries) { 284 versions.add(new BoxFileVersion(this.getAPI(), entry.asObject(), this.getID())); 285 } 286 287 return versions; 288 } 289 290 /** 291 * Uploads a new version of this file, replacing the current version. Note that only users with premium accounts 292 * will be able to view and recover previous versions of the file. 293 * @param fileContent a stream containing the new file contents. 294 */ 295 public void uploadVersion(InputStream fileContent) { 296 this.uploadVersion(fileContent, null); 297 } 298 299 /** 300 * Uploads a new version of this file, replacing the current version. Note that only users with premium accounts 301 * will be able to view and recover previous versions of the file. 302 * @param fileContent a stream containing the new file contents. 303 * @param modified the date that the new version was modified. 304 */ 305 public void uploadVersion(InputStream fileContent, Date modified) { 306 this.uploadVersion(fileContent, modified, 0, null); 307 } 308 309 /** 310 * Uploads a new version of this file, replacing the current version, while reporting the progress to a 311 * ProgressListener. Note that only users with premium accounts will be able to view and recover previous versions 312 * of the file. 313 * @param fileContent a stream containing the new file contents. 314 * @param modified the date that the new version was modified. 315 * @param fileSize the size of the file used for determining the progress of the upload. 316 * @param listener a listener for monitoring the upload's progress. 317 */ 318 public void uploadVersion(InputStream fileContent, Date modified, long fileSize, ProgressListener listener) { 319 URL uploadURL = CONTENT_URL_TEMPLATE.build(this.getAPI().getBaseUploadURL(), this.getID()); 320 BoxMultipartRequest request = new BoxMultipartRequest(getAPI(), uploadURL); 321 if (fileSize > 0) { 322 request.setFile(fileContent, "", fileSize); 323 } else { 324 request.setFile(fileContent, ""); 325 } 326 327 if (modified != null) { 328 request.putField("content_modified_at", modified); 329 } 330 331 BoxAPIResponse response; 332 if (listener == null) { 333 response = request.send(); 334 } else { 335 response = request.send(listener); 336 } 337 response.disconnect(); 338 } 339 340 /** 341 * Gets a list of any comments on this file. 342 * @return a list of comments on this file. 343 */ 344 public List<BoxComment.Info> getComments() { 345 URL url = GET_COMMENTS_URL_TEMPLATE.build(this.getAPI().getBaseURL(), this.getID()); 346 BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET"); 347 BoxJSONResponse response = (BoxJSONResponse) request.send(); 348 JsonObject responseJSON = JsonObject.readFrom(response.getJSON()); 349 350 int totalCount = responseJSON.get("total_count").asInt(); 351 List<BoxComment.Info> comments = new ArrayList<BoxComment.Info>(totalCount); 352 JsonArray entries = responseJSON.get("entries").asArray(); 353 for (JsonValue value : entries) { 354 JsonObject commentJSON = value.asObject(); 355 BoxComment comment = new BoxComment(this.getAPI(), commentJSON.get("id").asString()); 356 BoxComment.Info info = comment.new Info(commentJSON); 357 comments.add(info); 358 } 359 360 return comments; 361 } 362 363 /** 364 * Contains information about a BoxFile. 365 */ 366 public class Info extends BoxItem.Info { 367 private String sha1; 368 private String versionNumber; 369 private long commentCount; 370 private EnumSet<Permission> permissions; 371 private String extension; 372 private boolean isPackage; 373 374 /** 375 * Constructs an empty Info object. 376 */ 377 public Info() { 378 super(); 379 } 380 381 /** 382 * Constructs an Info object by parsing information from a JSON string. 383 * @param json the JSON string to parse. 384 */ 385 public Info(String json) { 386 super(json); 387 } 388 389 /** 390 * Constructs an Info object using an already parsed JSON object. 391 * @param jsonObject the parsed JSON object. 392 */ 393 Info(JsonObject jsonObject) { 394 super(jsonObject); 395 } 396 397 @Override 398 public BoxFile getResource() { 399 return BoxFile.this; 400 } 401 402 /** 403 * Gets the SHA1 hash of the file. 404 * @return the SHA1 hash of the file. 405 */ 406 public String getSha1() { 407 return this.sha1; 408 } 409 410 /** 411 * Gets the current version number of the file. 412 * @return the current version number of the file. 413 */ 414 public String getVersionNumber() { 415 return this.versionNumber; 416 } 417 418 /** 419 * Gets the number of comments on the file. 420 * @return the number of comments on the file. 421 */ 422 public long getCommentCount() { 423 return this.commentCount; 424 } 425 426 /** 427 * Gets the permissions that the current user has on the file. 428 * @return the permissions that the current user has on the file. 429 */ 430 public EnumSet<Permission> getPermissions() { 431 return this.permissions; 432 } 433 434 /** 435 * Gets the extension suffix of the file, excluding the dot. 436 * @return the extension of the file. 437 */ 438 public String getExtension() { 439 return this.extension; 440 } 441 442 /** 443 * Gets whether or not the file is an OSX package. 444 * @return true if the file is an OSX package; otherwise false. 445 */ 446 public boolean getIsPackage() { 447 return this.isPackage; 448 } 449 450 @Override 451 protected void parseJSONMember(JsonObject.Member member) { 452 super.parseJSONMember(member); 453 454 String memberName = member.getName(); 455 JsonValue value = member.getValue(); 456 switch (memberName) { 457 case "sha1": 458 this.sha1 = value.asString(); 459 break; 460 case "version_number": 461 this.versionNumber = value.asString(); 462 break; 463 case "comment_count": 464 this.commentCount = value.asLong(); 465 break; 466 case "permissions": 467 this.permissions = this.parsePermissions(value.asObject()); 468 break; 469 case "extension": 470 this.extension = value.asString(); 471 break; 472 case "is_package": 473 this.isPackage = value.asBoolean(); 474 break; 475 default: 476 break; 477 } 478 } 479 480 private EnumSet<Permission> parsePermissions(JsonObject jsonObject) { 481 EnumSet<Permission> permissions = EnumSet.noneOf(Permission.class); 482 for (JsonObject.Member member : jsonObject) { 483 JsonValue value = member.getValue(); 484 if (value.isNull() || !value.asBoolean()) { 485 continue; 486 } 487 488 String memberName = member.getName(); 489 switch (memberName) { 490 case "can_download": 491 permissions.add(Permission.CAN_DOWNLOAD); 492 break; 493 case "can_upload": 494 permissions.add(Permission.CAN_UPLOAD); 495 break; 496 case "can_rename": 497 permissions.add(Permission.CAN_RENAME); 498 break; 499 case "can_delete": 500 permissions.add(Permission.CAN_DELETE); 501 break; 502 case "can_share": 503 permissions.add(Permission.CAN_SHARE); 504 break; 505 case "can_set_share_access": 506 permissions.add(Permission.CAN_SET_SHARE_ACCESS); 507 break; 508 case "can_preview": 509 permissions.add(Permission.CAN_PREVIEW); 510 break; 511 case "can_comment": 512 permissions.add(Permission.CAN_COMMENT); 513 break; 514 default: 515 break; 516 } 517 } 518 519 return permissions; 520 } 521 } 522 523 /** 524 * Enumerates the possible permissions that a user can have on a file. 525 */ 526 public enum Permission { 527 /** 528 * The user can download the file. 529 */ 530 CAN_DOWNLOAD ("can_download"), 531 532 /** 533 * The user can upload new versions of the file. 534 */ 535 CAN_UPLOAD ("can_upload"), 536 537 /** 538 * The user can rename the file. 539 */ 540 CAN_RENAME ("can_rename"), 541 542 /** 543 * The user can delete the file. 544 */ 545 CAN_DELETE ("can_delete"), 546 547 /** 548 * The user can share the file. 549 */ 550 CAN_SHARE ("can_share"), 551 552 /** 553 * The user can set the access level for shared links to the file. 554 */ 555 CAN_SET_SHARE_ACCESS ("can_set_share_access"), 556 557 /** 558 * The user can preview the file. 559 */ 560 CAN_PREVIEW ("can_preview"), 561 562 /** 563 * The user can comment on the file. 564 */ 565 CAN_COMMENT ("can_comment"); 566 567 private final String jsonValue; 568 569 private Permission(String jsonValue) { 570 this.jsonValue = jsonValue; 571 } 572 573 static Permission fromJSONValue(String jsonValue) { 574 return Permission.valueOf(jsonValue.toUpperCase()); 575 } 576 577 String toJSONValue() { 578 return this.jsonValue; 579 } 580 } 581}