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}