From 4f0840045421257ba72489e41d6ae2d943553667 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 30 Mar 2016 12:49:04 +0200 Subject: [PATCH 1/8] Fix logger usage. --- .../b2/io/HttpMethodReleaseInputStream.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/main/java/synapticloop/b2/io/HttpMethodReleaseInputStream.java b/src/main/java/synapticloop/b2/io/HttpMethodReleaseInputStream.java index 36f2761..d1a1965 100644 --- a/src/main/java/synapticloop/b2/io/HttpMethodReleaseInputStream.java +++ b/src/main/java/synapticloop/b2/io/HttpMethodReleaseInputStream.java @@ -16,17 +16,17 @@ * this source code or binaries. */ -import java.io.IOException; -import java.util.logging.Level; -import java.util.logging.Logger; - import org.apache.commons.io.input.CountingInputStream; import org.apache.http.HttpConnection; import org.apache.http.HttpResponse; import org.apache.http.client.methods.CloseableHttpResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; public class HttpMethodReleaseInputStream extends CountingInputStream { - private static final Logger LOGGER = Logger.getLogger(HttpMethodReleaseInputStream.class.getName()); + private static final Logger LOGGER = LoggerFactory.getLogger(HttpMethodReleaseInputStream.class); private HttpResponse response; @@ -58,10 +58,7 @@ public void close() throws IOException { // Fully consumed super.close(); } else { - if(LOGGER.isLoggable(Level.WARNING)) { - LOGGER.warning(String.format("Abort connection for response '{}'", response)); - } - + LOGGER.warn("Abort connection for response '{}'", response); // Close an HTTP response as quickly as possible, avoiding consuming // response data unnecessarily though at the expense of making underlying // connections unavailable for reuse. From f788bfe9ddc04822869e25d4471795e2fd4da694 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 30 Mar 2016 13:09:10 +0200 Subject: [PATCH 2/8] Expose upload URI methods in client interface. The URL and authorization token that you get from b2_get_upload_url can be used by only one thread at a time. If you want multiple threads running, each one needs to get its own URL and auth token. It can keep using that URL and auth token for multiple uploads, until it gets a returned status indicating that it should get a new upload URL. --- .../java/synapticloop/b2/B2ApiClient.java | 212 ++++++++++++++---- 1 file changed, 167 insertions(+), 45 deletions(-) diff --git a/src/main/java/synapticloop/b2/B2ApiClient.java b/src/main/java/synapticloop/b2/B2ApiClient.java index 541d52c..9930bed 100644 --- a/src/main/java/synapticloop/b2/B2ApiClient.java +++ b/src/main/java/synapticloop/b2/B2ApiClient.java @@ -231,27 +231,60 @@ public B2DownloadFileResponse headFileById(String fileId) throws B2ApiException, * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + /** + * An uploadUrl and upload authorizationToken are valid for 24 hours or until the endpoint rejects an upload, see b2_upload_file. + * You can upload as many files to this URL as you need. To achieve faster upload speeds, request multiple uploadUrls and + * upload your files to these different endpoints in parallel. + * + * @param bucketId the id of the bucket to upload to + * @return Upload URL. Not be shared between threads but can be used for multiple parts + * @throws B2ApiException if there was an error with the request + * @throws IOException if there was an error communicating with the API service + */ + public B2GetUploadUrlResponse getUploadUrl(String bucketId) throws B2ApiException, IOException { + return new B2GetUploadUrlRequest(client, b2AuthorizeAccountResponse, bucketId).getResponse(); + } + /** * Upload a file to a bucket * - * @param bucketId the id of the bucket - * @param fileName the name of the file that will be placed in the bucket - * (including any path separators '/') - * @param entity the file content to upload + * @param bucketId the id of the bucket + * @param fileName the name of the file that will be placed in the bucket + * (including any path separators '/') + * @param entity the file content to upload * @param sha1Checksum the checksum for the file - * @param mimeType the mime type of the file, if null, then the mime type - * will be attempted to be automatically mapped by the backblaze B2 API - * see https://www.backblaze.com/b2/docs/content-types.html - * for a list of content type mappings. - * @param fileInfo the file info map which will be set as 'X-Bz-Info-' headers - * + * @param mimeType the mime type of the file, if null, then the mime type + * will be attempted to be automatically mapped by the backblaze B2 API + * see https://www.backblaze.com/b2/docs/content-types.html + * for a list of content type mappings. + * @param fileInfo the file info map which will be set as 'X-Bz-Info-' headers * @return the uploaded file response - * * @throws B2ApiException if there was an error uploading the file - * @throws IOException if there was an error communicating with the API service + * @throws IOException if there was an error communicating with the API service */ public B2FileResponse uploadFile(String bucketId, String fileName, HttpEntity entity, String sha1Checksum, String mimeType, Map fileInfo) throws B2ApiException, IOException { - B2GetUploadUrlResponse b2GetUploadUrlResponse = new B2GetUploadUrlRequest(client, b2AuthorizeAccountResponse, bucketId).getResponse(); + B2GetUploadUrlResponse b2GetUploadUrlResponse = this.getUploadUrl(bucketId); + return this.uploadFile(b2GetUploadUrlResponse, fileName, entity, sha1Checksum, mimeType, fileInfo); + } + + /** + * Upload a file to a bucket + * + * @param b2GetUploadUrlResponse Upload URL. + * @param fileName the name of the file that will be placed in the bucket + * (including any path separators '/') + * @param entity the file content to upload + * @param sha1Checksum the checksum for the file + * @param mimeType the mime type of the file, if null, then the mime type + * will be attempted to be automatically mapped by the backblaze B2 API + * see https://www.backblaze.com/b2/docs/content-types.html + * for a list of content type mappings. + * @param fileInfo the file info map which will be set as 'X-Bz-Info-' headers + * @return the uploaded file response + * @throws B2ApiException if there was an error uploading the file + * @throws IOException if there was an error communicating with the API service + */ + public B2FileResponse uploadFile(B2GetUploadUrlResponse b2GetUploadUrlResponse, String fileName, HttpEntity entity, String sha1Checksum, String mimeType, Map fileInfo) throws B2ApiException, IOException { return new B2UploadFileRequest(client, b2AuthorizeAccountResponse, b2GetUploadUrlResponse, fileName, entity, sha1Checksum, mimeType, fileInfo).getResponse(); } @@ -260,21 +293,39 @@ public B2FileResponse uploadFile(String bucketId, String fileName, HttpEntity en * * @param bucketId the id of the bucket * @param fileName the name of the file that will be placed in the bucket - * (including any path separators '/') - * @param file the file to upload + * (including any path separators '/') + * @param file the file to upload * @param mimeType the mime type of the file, if null, then the mime type - * will be attempted to be automatically mapped by the backblaze B2 API - * see https://www.backblaze.com/b2/docs/content-types.html - * for a list of content type mappings. + * will be attempted to be automatically mapped by the backblaze B2 API + * see https://www.backblaze.com/b2/docs/content-types.html + * for a list of content type mappings. * @param fileInfo the file info map which will be set as 'X-Bz-Info-' headers - * * @return the uploaded file response - * * @throws B2ApiException if there was an error uploading the file - * @throws IOException if there was an error communicating with the API service + * @throws IOException if there was an error communicating with the API service */ public B2FileResponse uploadFile(String bucketId, String fileName, File file, String mimeType, Map fileInfo) throws B2ApiException, IOException { - B2GetUploadUrlResponse b2GetUploadUrlResponse = new B2GetUploadUrlRequest(client, b2AuthorizeAccountResponse, bucketId).getResponse(); + B2GetUploadUrlResponse b2GetUploadUrlResponse = this.getUploadUrl(bucketId); + return this.uploadFile(b2GetUploadUrlResponse, fileName, file, mimeType, fileInfo); + } + + /** + * Upload a file to a bucket + * + * @param b2GetUploadUrlResponse Upload URL. + * @param fileName the name of the file that will be placed in the bucket + * (including any path separators '/') + * @param file the file to upload + * @param mimeType the mime type of the file, if null, then the mime type + * will be attempted to be automatically mapped by the backblaze B2 API + * see https://www.backblaze.com/b2/docs/content-types.html + * for a list of content type mappings. + * @param fileInfo the file info map which will be set as 'X-Bz-Info-' headers + * @return the uploaded file response + * @throws B2ApiException if there was an error uploading the file + * @throws IOException if there was an error communicating with the API service + */ + public B2FileResponse uploadFile(B2GetUploadUrlResponse b2GetUploadUrlResponse, String fileName, File file, String mimeType, Map fileInfo) throws B2ApiException, IOException { return new B2UploadFileRequest(client, b2AuthorizeAccountResponse, b2GetUploadUrlResponse, fileName, file, ChecksumHelper.calculateSha1(file), mimeType, fileInfo).getResponse(); } @@ -284,18 +335,31 @@ public B2FileResponse uploadFile(String bucketId, String fileName, File file, St * * @param bucketId the id of the bucket * @param fileName the name of the file that will be placed in the bucket - * (including any path separators '/') - * @param file the file to upload + * (including any path separators '/') + * @param file the file to upload * @param fileInfo the file info map which will be set as 'X-Bz-Info-' headers - * * @return the uploaded file response - * * @throws B2ApiException if there was an error uploading the file - * @throws IOException if there was an error communicating with the API service + * @throws IOException if there was an error communicating with the API service */ - public B2FileResponse uploadFile(String bucketId, String fileName, File file, Map fileInfo) throws B2ApiException, IOException { - B2GetUploadUrlResponse b2GetUploadUrlResponse = new B2GetUploadUrlRequest(client, b2AuthorizeAccountResponse, bucketId).getResponse(); + B2GetUploadUrlResponse b2GetUploadUrlResponse = this.getUploadUrl(bucketId); + return this.uploadFile(b2GetUploadUrlResponse, fileName, file, fileInfo); + } + + /** + * Upload a file to a bucket + * + * @param b2GetUploadUrlResponse Upload URL. + * @param fileName the name of the file that will be placed in the bucket + * (including any path separators '/') + * @param file the file to upload + * @param fileInfo the file info map which will be set as 'X-Bz-Info-' headers + * @return the uploaded file response + * @throws B2ApiException if there was an error uploading the file + * @throws IOException if there was an error communicating with the API service + */ + public B2FileResponse uploadFile(B2GetUploadUrlResponse b2GetUploadUrlResponse, String fileName, File file, Map fileInfo) throws B2ApiException, IOException { return new B2UploadFileRequest(client, b2AuthorizeAccountResponse, b2GetUploadUrlResponse, fileName, file, ChecksumHelper.calculateSha1(file), fileInfo).getResponse(); } @@ -305,20 +369,37 @@ public B2FileResponse uploadFile(String bucketId, String fileName, File file, Ma * * @param bucketId the id of the bucket * @param fileName the name of the file that will be placed in the bucket - * (including any path separators '/') - * @param file the file to upload + * (including any path separators '/') + * @param file the file to upload * @param mimeType the mime type of the file, if null, then the mime type - * will be attempted to be automatically mapped by the backblaze B2 API - * see https://www.backblaze.com/b2/docs/content-types.html - * for a list of content type mappings. - * + * will be attempted to be automatically mapped by the backblaze B2 API + * see https://www.backblaze.com/b2/docs/content-types.html + * for a list of content type mappings. * @return the uploaded file response - * * @throws B2ApiException if there was an error uploading the file - * @throws IOException if there was an error communicating with the API service + * @throws IOException if there was an error communicating with the API service */ public B2FileResponse uploadFile(String bucketId, String fileName, File file, String mimeType) throws B2ApiException, IOException { - B2GetUploadUrlResponse b2GetUploadUrlResponse = new B2GetUploadUrlRequest(client, b2AuthorizeAccountResponse, bucketId).getResponse(); + B2GetUploadUrlResponse b2GetUploadUrlResponse = this.getUploadUrl(bucketId); + return this.uploadFile(b2GetUploadUrlResponse, fileName, file, mimeType); + } + + /** + * Upload a file to a bucket + * + * @param b2GetUploadUrlResponse Upload URL. + * @param fileName the name of the file that will be placed in the bucket + * (including any path separators '/') + * @param file the file to upload + * @param mimeType the mime type of the file, if null, then the mime type + * will be attempted to be automatically mapped by the backblaze B2 API + * see https://www.backblaze.com/b2/docs/content-types.html + * for a list of content type mappings. + * @return the uploaded file response + * @throws B2ApiException if there was an error uploading the file + * @throws IOException if there was an error communicating with the API service + */ + public B2FileResponse uploadFile(B2GetUploadUrlResponse b2GetUploadUrlResponse, String fileName, File file, String mimeType) throws B2ApiException, IOException { return new B2UploadFileRequest(client, b2AuthorizeAccountResponse, b2GetUploadUrlResponse, fileName, file, ChecksumHelper.calculateSha1(file), mimeType).getResponse(); } @@ -329,16 +410,30 @@ public B2FileResponse uploadFile(String bucketId, String fileName, File file, St * * @param bucketId the id of the bucket * @param fileName the name of the file that will be placed in the bucket - * (including any path separators '/') - * @param file the file to upload - * + * (including any path separators '/') + * @param file the file to upload * @return the uploaded file response - * * @throws B2ApiException if there was an error uploading the file - * @throws IOException if there was an error communicating with the API service + * @throws IOException if there was an error communicating with the API service */ public B2FileResponse uploadFile(String bucketId, String fileName, File file) throws B2ApiException, IOException { - B2GetUploadUrlResponse b2GetUploadUrlResponse = new B2GetUploadUrlRequest(client, b2AuthorizeAccountResponse, bucketId).getResponse(); + B2GetUploadUrlResponse b2GetUploadUrlResponse = this.getUploadUrl(bucketId); + return this.uploadFile(b2GetUploadUrlResponse, fileName, file); + } + + /** + * Upload a file to a bucket, the mimetype will be automatically set by the + * back-end B2 API system + * + * @param b2GetUploadUrlResponse Upload URL. + * @param fileName the name of the file that will be placed in the bucket + * (including any path separators '/') + * @param file the file to upload + * @return the uploaded file response + * @throws B2ApiException if there was an error uploading the file + * @throws IOException if there was an error communicating with the API service + */ + public B2FileResponse uploadFile(B2GetUploadUrlResponse b2GetUploadUrlResponse, String fileName, File file) throws B2ApiException, IOException { return new B2UploadFileRequest(client, b2AuthorizeAccountResponse, b2GetUploadUrlResponse, fileName, file, ChecksumHelper.calculateSha1(file)).getResponse(); } @@ -349,6 +444,19 @@ public B2FileResponse uploadFile(String bucketId, String fileName, File file) th * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + /** + * When you upload part of a large file to B2, you must call b2_get_upload_part_url first to get the URL for uploading. + * Then, you use b2_upload_part on this URL to upload your data. + * + * @param fileId the id of the file to upload + * @return Upload URL. Not be shared between threads but can be used for multiple parts + * @throws B2ApiException if there was an error with the request + * @throws IOException if there was an error communicating with the API service + */ + public B2GetUploadPartUrlResponse getUploadPartUrl(String fileId) throws B2ApiException, IOException { + return new B2GetUploadPartUrlRequest(client, b2AuthorizeAccountResponse, fileId).getResponse(); + } + /** * Start large file upload * @@ -399,7 +507,21 @@ public B2FinishLargeFileResponse finishLargeFileUpload(String fileId, String[] p * @throws B2ApiException if there was an error uploading the file */ public B2UploadPartResponse uploadLargeFilePart(String fileId, int partNumber, HttpEntity entity, String sha1Checksum) throws B2ApiException, IOException { - final B2GetUploadPartUrlResponse b2GetUploadUrlResponse = new B2GetUploadPartUrlRequest(client, b2AuthorizeAccountResponse, fileId).getResponse(); + final B2GetUploadPartUrlResponse b2GetUploadUrlResponse = this.getUploadPartUrl(fileId); + return this.uploadLargeFilePart(b2GetUploadUrlResponse, partNumber, entity, sha1Checksum); + } + + /** + * Upload large file upload part + * + * @param b2GetUploadUrlResponse Upload URL. + * @param partNumber A number from 1 to 10000. The parts uploaded for one file must have contiguous numbers, starting with 1. + * @param entity Part content body + * @param sha1Checksum the checksum for the part + * @return Upload response + * @throws B2ApiException if there was an error uploading the file + */ + public B2UploadPartResponse uploadLargeFilePart(B2GetUploadPartUrlResponse b2GetUploadUrlResponse, int partNumber, HttpEntity entity, String sha1Checksum) throws B2ApiException, IOException { return new B2UploadPartRequest(client, b2AuthorizeAccountResponse, b2GetUploadUrlResponse, partNumber, entity, sha1Checksum).getResponse(); } From ffe588505916f43f70bafe2cf275006ee1d70629 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 30 Mar 2016 13:09:58 +0200 Subject: [PATCH 3/8] Formatting. --- .../b2/response/BaseB2Response.java | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/main/java/synapticloop/b2/response/BaseB2Response.java b/src/main/java/synapticloop/b2/response/BaseB2Response.java index f060900..eecc6b9 100644 --- a/src/main/java/synapticloop/b2/response/BaseB2Response.java +++ b/src/main/java/synapticloop/b2/response/BaseB2Response.java @@ -1,21 +1,18 @@ package synapticloop.b2.response; -import java.io.IOException; -import java.util.Iterator; - /* * Copyright (c) 2016 Synapticloop. - * + * * All rights reserved. - * - * This code may contain contributions from other parties which, where - * applicable, will be listed in the default build file for the project + * + * This code may contain contributions from other parties which, where + * applicable, will be listed in the default build file for the project * ~and/or~ in a file named CONTRIBUTORS.txt in the root of the project. - * - * This source code and any derived binaries are covered by the terms and - * conditions of the Licence agreement ("the Licence"). You may not use this - * source code or any derived binaries except in compliance with the Licence. - * A copy of the Licence is available in the file named LICENSE.txt shipped with + * + * This source code and any derived binaries are covered by the terms and + * conditions of the Licence agreement ("the Licence"). You may not use this + * source code or any derived binaries except in compliance with the Licence. + * A copy of the Licence is available in the file named LICENSE.txt shipped with * this source code or binaries. */ @@ -23,9 +20,10 @@ import org.json.JSONException; import org.json.JSONObject; import org.slf4j.Logger; - import synapticloop.b2.exception.B2ApiException; +import java.util.Iterator; + public abstract class BaseB2Response { private final JSONObject response; From 136e9760e7faca59e9f5e7900f4a4efd90c9edf3 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 30 Mar 2016 13:10:11 +0200 Subject: [PATCH 4/8] Expect test not to fail. --- .../synapticloop/b2/request/B2StartLargeFileRequestTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/java/synapticloop/b2/request/B2StartLargeFileRequestTest.java b/src/test/java/synapticloop/b2/request/B2StartLargeFileRequestTest.java index c8ef3b7..489dca7 100644 --- a/src/test/java/synapticloop/b2/request/B2StartLargeFileRequestTest.java +++ b/src/test/java/synapticloop/b2/request/B2StartLargeFileRequestTest.java @@ -21,8 +21,7 @@ public class B2StartLargeFileRequestTest { - // this is expected until the large file support goes live - @Test(expected=B2ApiException.class) + @Test public void getResponse() throws Exception { B2AuthorizeAccountResponse b2AuthorizeAccountResponse = B2TestHelper.getB2AuthorizeAccountResponse(); From e83dec32cbcc7255f7f1ba622fcbd8c2a7a90a5f Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 30 Mar 2016 16:47:22 +0200 Subject: [PATCH 5/8] Add action field. --- .../synapticloop/b2/response/B2FileInfoResponse.java | 4 ---- .../synapticloop/b2/response/B2FileResponse.java | 12 ++++++++++++ .../b2/response/B2FinishLargeFileResponse.java | 12 +++++++----- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/main/java/synapticloop/b2/response/B2FileInfoResponse.java b/src/main/java/synapticloop/b2/response/B2FileInfoResponse.java index 5df47de..cf69622 100644 --- a/src/main/java/synapticloop/b2/response/B2FileInfoResponse.java +++ b/src/main/java/synapticloop/b2/response/B2FileInfoResponse.java @@ -76,11 +76,7 @@ public B2FileInfoResponse(final JSONObject response) throws B2ApiException { } catch(IllegalArgumentException e) { LOGGER.warn("Unknown action value " + action); - this.action = null; } - } else { - // Default - this.action = Action.upload; } this.size = this.readLong(B2ResponseProperties.KEY_SIZE); diff --git a/src/main/java/synapticloop/b2/response/B2FileResponse.java b/src/main/java/synapticloop/b2/response/B2FileResponse.java index 0c89a0a..b6a6b70 100644 --- a/src/main/java/synapticloop/b2/response/B2FileResponse.java +++ b/src/main/java/synapticloop/b2/response/B2FileResponse.java @@ -24,6 +24,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import synapticloop.b2.Action; import synapticloop.b2.exception.B2ApiException; public class B2FileResponse extends BaseB2Response { @@ -37,6 +38,7 @@ public class B2FileResponse extends BaseB2Response { private final String contentSha1; private final String contentType; private final Map fileInfo; + private Action action; /** * Instantiate a file response with the JSON response as a string from @@ -69,6 +71,16 @@ public B2FileResponse(String json) throws B2ApiException { } } + String action = this.readString(B2ResponseProperties.KEY_ACTION); + if(null != action) { + try { + this.action = Action.valueOf(action); + } + catch(IllegalArgumentException e) { + LOGGER.warn("Unknown action value " + action); + } + } + this.warnOnMissedKeys(); } diff --git a/src/main/java/synapticloop/b2/response/B2FinishLargeFileResponse.java b/src/main/java/synapticloop/b2/response/B2FinishLargeFileResponse.java index c4f8d8c..35edce6 100644 --- a/src/main/java/synapticloop/b2/response/B2FinishLargeFileResponse.java +++ b/src/main/java/synapticloop/b2/response/B2FinishLargeFileResponse.java @@ -38,7 +38,7 @@ public class B2FinishLargeFileResponse extends BaseB2Response { private final String contentType; private final Map fileInfo; - private final Action action; + private Action action; public B2FinishLargeFileResponse(final String json) throws B2ApiException { super(json); @@ -63,10 +63,12 @@ public B2FinishLargeFileResponse(final String json) throws B2ApiException { String action = this.readString(B2ResponseProperties.KEY_ACTION); if(null != action) { - this.action = Action.valueOf(action); - } else { - // Default - this.action = Action.upload; + try { + this.action = Action.valueOf(action); + } + catch(IllegalArgumentException e) { + LOGGER.warn("Unknown action value " + action); + } } this.warnOnMissedKeys(); From d5dc25c42aa30814e8e9cb5942a1e197b919e115 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 30 Mar 2016 21:18:22 +0200 Subject: [PATCH 6/8] Fix concurrent modification when iterating keys from fileInfo. --- .../b2/response/B2FileInfoResponse.java | 5 +-- .../b2/response/B2FileResponse.java | 6 +-- .../response/B2FinishLargeFileResponse.java | 6 +-- .../b2/response/B2StartLargeFileResponse.java | 4 +- .../b2/response/B2FileInfoResponseTest.java | 42 +++++++++++++++++++ 5 files changed, 46 insertions(+), 17 deletions(-) create mode 100644 src/test/java/synapticloop/b2/response/B2FileInfoResponseTest.java diff --git a/src/main/java/synapticloop/b2/response/B2FileInfoResponse.java b/src/main/java/synapticloop/b2/response/B2FileInfoResponse.java index 5df47de..fde8157 100644 --- a/src/main/java/synapticloop/b2/response/B2FileInfoResponse.java +++ b/src/main/java/synapticloop/b2/response/B2FileInfoResponse.java @@ -59,12 +59,9 @@ public B2FileInfoResponse(final JSONObject response) throws B2ApiException { this.contentType = this.readString(B2ResponseProperties.KEY_CONTENT_TYPE); this.contentSha1 = this.readString(B2ResponseProperties.KEY_CONTENT_SHA1); this.fileInfo = new HashMap(); - JSONObject fileInfoObject = this.readObject(B2ResponseProperties.KEY_FILE_INFO); if(null != fileInfoObject) { - Iterator keys = fileInfoObject.keys(); - while (keys.hasNext()) { - String key = (String) keys.next(); + for (String key: fileInfoObject.keySet().toArray(new String[fileInfoObject.keySet().size()])) { fileInfo.put(key, this.readString(fileInfoObject, key)); } } diff --git a/src/main/java/synapticloop/b2/response/B2FileResponse.java b/src/main/java/synapticloop/b2/response/B2FileResponse.java index 0c89a0a..8d05dfe 100644 --- a/src/main/java/synapticloop/b2/response/B2FileResponse.java +++ b/src/main/java/synapticloop/b2/response/B2FileResponse.java @@ -57,14 +57,10 @@ public B2FileResponse(String json) throws B2ApiException { this.contentLength = this.readLong(B2ResponseProperties.KEY_CONTENT_LENGTH); this.contentSha1 = this.readString(B2ResponseProperties.KEY_CONTENT_SHA1); this.contentType = this.readString(B2ResponseProperties.KEY_CONTENT_TYPE); - this.fileInfo = new HashMap(); - JSONObject fileInfoObject = this.readObject(B2ResponseProperties.KEY_FILE_INFO); if(null != fileInfoObject) { - Iterator keys = fileInfoObject.keys(); - while (keys.hasNext()) { - String key = (String) keys.next(); + for (String key: fileInfoObject.keySet().toArray(new String[fileInfoObject.keySet().size()])) { fileInfo.put(key, this.readString(fileInfoObject, key)); } } diff --git a/src/main/java/synapticloop/b2/response/B2FinishLargeFileResponse.java b/src/main/java/synapticloop/b2/response/B2FinishLargeFileResponse.java index c4f8d8c..09f7fc3 100644 --- a/src/main/java/synapticloop/b2/response/B2FinishLargeFileResponse.java +++ b/src/main/java/synapticloop/b2/response/B2FinishLargeFileResponse.java @@ -51,16 +51,12 @@ public B2FinishLargeFileResponse(final String json) throws B2ApiException { this.contentType = this.readString(B2ResponseProperties.KEY_CONTENT_TYPE); this.contentSha1 = this.readString(B2ResponseProperties.KEY_CONTENT_SHA1); this.fileInfo = new HashMap(); - JSONObject fileInfoObject = this.readObject(B2ResponseProperties.KEY_FILE_INFO); if(null != fileInfoObject) { - Iterator keys = fileInfoObject.keys(); - while (keys.hasNext()) { - String key = (String) keys.next(); + for (String key: fileInfoObject.keySet().toArray(new String[fileInfoObject.keySet().size()])) { fileInfo.put(key, this.readString(fileInfoObject, key)); } } - String action = this.readString(B2ResponseProperties.KEY_ACTION); if(null != action) { this.action = Action.valueOf(action); diff --git a/src/main/java/synapticloop/b2/response/B2StartLargeFileResponse.java b/src/main/java/synapticloop/b2/response/B2StartLargeFileResponse.java index 6173626..ce6e189 100644 --- a/src/main/java/synapticloop/b2/response/B2StartLargeFileResponse.java +++ b/src/main/java/synapticloop/b2/response/B2StartLargeFileResponse.java @@ -46,9 +46,7 @@ public B2StartLargeFileResponse(String json) throws B2ApiException { this.fileInfo = new HashMap(); JSONObject fileInfoObject = this.readObject(B2ResponseProperties.KEY_FILE_INFO); if (null != fileInfoObject) { - Iterator keys = fileInfoObject.keys(); - while (keys.hasNext()) { - String key = (String) keys.next(); + for (String key: fileInfoObject.keySet().toArray(new String[fileInfoObject.keySet().size()])) { fileInfo.put(key, fileInfoObject.opt(key)); } } diff --git a/src/test/java/synapticloop/b2/response/B2FileInfoResponseTest.java b/src/test/java/synapticloop/b2/response/B2FileInfoResponseTest.java new file mode 100644 index 0000000..e1792e2 --- /dev/null +++ b/src/test/java/synapticloop/b2/response/B2FileInfoResponseTest.java @@ -0,0 +1,42 @@ +package synapticloop.b2.response; + +import org.json.JSONObject; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class B2FileInfoResponseTest { + + @Test + public void testGetFileInfo() throws Exception { + final String json = + " {\n" + + " \"action\": \"upload\",\n" + + " \"contentSha1\": \"f66ccfc5920c4527a3473cac8418e759fc991a1c\",\n" + + " \"contentType\": \"image/jpeg\",\n" + + " \"fileId\": \"4_zd866c2e0ac0d7d4855110b15_f10583db69f768e88_d20160122_m203004_c000_v0001016_t0021\",\n" + + " \"fileInfo\": {\n" + + " \"meta1\": \"This is some text for meta1.\",\n" + + " \"meta2\": \"This is some more text for meta2.\",\n" + + " \"unicorns-and-rainbows\": \"test header\"\n" + + " },\n" + + " \"fileName\": \"IMG_4666.jpg\",\n" + + " \"size\": 1828156,\n" + + " \"uploadTimestamp\": 1453494604000\n" + + " }"; + final B2FileInfoResponse response = new B2FileInfoResponse(new JSONObject(json)); + assertNotNull(response); + assertNotNull(response.getFileId()); + assertNull(response.getContentLength()); + assertNotNull(response.getAction()); + assertNotNull(response.getFileName()); + assertNotNull(response.getContentSha1()); + assertNotNull(response.getSize()); + assertNotNull(response.getFileInfo()); + assertFalse(response.getFileInfo().isEmpty()); + assertNotNull(response.getFileInfo().get("meta1")); + assertNotNull(response.getFileInfo().get("meta1")); + assertNotNull(response.getFileInfo().get("meta2")); + assertNotNull(response.getFileInfo().get("unicorns-and-rainbows")); + } +} \ No newline at end of file From 04db8bab40de6044ccdb89f7aef2a290825f031b Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 30 Mar 2016 21:26:47 +0200 Subject: [PATCH 7/8] Extract method. --- .../b2/response/B2FileInfoResponse.java | 9 +------- .../b2/response/B2FileResponse.java | 8 +------ .../response/B2FinishLargeFileResponse.java | 8 +------ .../b2/response/B2StartLargeFileResponse.java | 12 +++-------- .../b2/response/BaseB2Response.java | 21 +++++++++++++++++++ 5 files changed, 27 insertions(+), 31 deletions(-) diff --git a/src/main/java/synapticloop/b2/response/B2FileInfoResponse.java b/src/main/java/synapticloop/b2/response/B2FileInfoResponse.java index fde8157..b411811 100644 --- a/src/main/java/synapticloop/b2/response/B2FileInfoResponse.java +++ b/src/main/java/synapticloop/b2/response/B2FileInfoResponse.java @@ -58,14 +58,7 @@ public B2FileInfoResponse(final JSONObject response) throws B2ApiException { this.contentLength = this.readLong(B2ResponseProperties.KEY_CONTENT_LENGTH); this.contentType = this.readString(B2ResponseProperties.KEY_CONTENT_TYPE); this.contentSha1 = this.readString(B2ResponseProperties.KEY_CONTENT_SHA1); - this.fileInfo = new HashMap(); - JSONObject fileInfoObject = this.readObject(B2ResponseProperties.KEY_FILE_INFO); - if(null != fileInfoObject) { - for (String key: fileInfoObject.keySet().toArray(new String[fileInfoObject.keySet().size()])) { - fileInfo.put(key, this.readString(fileInfoObject, key)); - } - } - + this.fileInfo = this.readMap(B2ResponseProperties.KEY_FILE_INFO); String action = this.readString(B2ResponseProperties.KEY_ACTION); if(null != action) { try { diff --git a/src/main/java/synapticloop/b2/response/B2FileResponse.java b/src/main/java/synapticloop/b2/response/B2FileResponse.java index 8d05dfe..d0697de 100644 --- a/src/main/java/synapticloop/b2/response/B2FileResponse.java +++ b/src/main/java/synapticloop/b2/response/B2FileResponse.java @@ -57,13 +57,7 @@ public B2FileResponse(String json) throws B2ApiException { this.contentLength = this.readLong(B2ResponseProperties.KEY_CONTENT_LENGTH); this.contentSha1 = this.readString(B2ResponseProperties.KEY_CONTENT_SHA1); this.contentType = this.readString(B2ResponseProperties.KEY_CONTENT_TYPE); - this.fileInfo = new HashMap(); - JSONObject fileInfoObject = this.readObject(B2ResponseProperties.KEY_FILE_INFO); - if(null != fileInfoObject) { - for (String key: fileInfoObject.keySet().toArray(new String[fileInfoObject.keySet().size()])) { - fileInfo.put(key, this.readString(fileInfoObject, key)); - } - } + this.fileInfo = this.readMap(B2ResponseProperties.KEY_FILE_INFO); this.warnOnMissedKeys(); } diff --git a/src/main/java/synapticloop/b2/response/B2FinishLargeFileResponse.java b/src/main/java/synapticloop/b2/response/B2FinishLargeFileResponse.java index 09f7fc3..dae9f43 100644 --- a/src/main/java/synapticloop/b2/response/B2FinishLargeFileResponse.java +++ b/src/main/java/synapticloop/b2/response/B2FinishLargeFileResponse.java @@ -50,13 +50,7 @@ public B2FinishLargeFileResponse(final String json) throws B2ApiException { this.contentLength = this.readLong(B2ResponseProperties.KEY_CONTENT_LENGTH); this.contentType = this.readString(B2ResponseProperties.KEY_CONTENT_TYPE); this.contentSha1 = this.readString(B2ResponseProperties.KEY_CONTENT_SHA1); - this.fileInfo = new HashMap(); - JSONObject fileInfoObject = this.readObject(B2ResponseProperties.KEY_FILE_INFO); - if(null != fileInfoObject) { - for (String key: fileInfoObject.keySet().toArray(new String[fileInfoObject.keySet().size()])) { - fileInfo.put(key, this.readString(fileInfoObject, key)); - } - } + this.fileInfo = this.readMap(B2ResponseProperties.KEY_FILE_INFO); String action = this.readString(B2ResponseProperties.KEY_ACTION); if(null != action) { this.action = Action.valueOf(action); diff --git a/src/main/java/synapticloop/b2/response/B2StartLargeFileResponse.java b/src/main/java/synapticloop/b2/response/B2StartLargeFileResponse.java index ce6e189..b9676bc 100644 --- a/src/main/java/synapticloop/b2/response/B2StartLargeFileResponse.java +++ b/src/main/java/synapticloop/b2/response/B2StartLargeFileResponse.java @@ -33,7 +33,7 @@ public class B2StartLargeFileResponse extends BaseB2Response { private final String accountId; private final String bucketId; private final String contentType; - private final Map fileInfo; + private final Map fileInfo; public B2StartLargeFileResponse(String json) throws B2ApiException { super(json); @@ -43,13 +43,7 @@ public B2StartLargeFileResponse(String json) throws B2ApiException { this.accountId = this.readString(B2ResponseProperties.KEY_ACCOUNT_ID); this.bucketId = this.readString(B2ResponseProperties.KEY_BUCKET_ID); this.contentType = this.readString(B2ResponseProperties.KEY_CONTENT_TYPE); - this.fileInfo = new HashMap(); - JSONObject fileInfoObject = this.readObject(B2ResponseProperties.KEY_FILE_INFO); - if (null != fileInfoObject) { - for (String key: fileInfoObject.keySet().toArray(new String[fileInfoObject.keySet().size()])) { - fileInfo.put(key, fileInfoObject.opt(key)); - } - } + this.fileInfo = this.readMap(B2ResponseProperties.KEY_FILE_INFO); this.warnOnMissedKeys(); } @@ -64,7 +58,7 @@ public B2StartLargeFileResponse(String json) throws B2ApiException { public String getContentType() { return this.contentType; } - public Map getFileInfo() { return this.fileInfo; } + public Map getFileInfo() { return this.fileInfo; } @Override protected Logger getLogger() { return LOGGER; } diff --git a/src/main/java/synapticloop/b2/response/BaseB2Response.java b/src/main/java/synapticloop/b2/response/BaseB2Response.java index eecc6b9..bdccd4f 100644 --- a/src/main/java/synapticloop/b2/response/BaseB2Response.java +++ b/src/main/java/synapticloop/b2/response/BaseB2Response.java @@ -22,7 +22,9 @@ import org.slf4j.Logger; import synapticloop.b2.exception.B2ApiException; +import java.util.HashMap; import java.util.Iterator; +import java.util.Map; public abstract class BaseB2Response { private final JSONObject response; @@ -169,6 +171,25 @@ protected JSONArray readObjects(String key) { return value instanceof JSONArray ? (JSONArray) value : null; } + /** + * Read and remove JSONObject with key from JSON object + * + * @param key the key to read as a JSONObject and put keys and values into map + * + * @return the read keys and values (or null if it doesn't exist) + */ + protected Map readMap(String key) { + final Map map = new HashMap(); + JSONObject value = this.readObject(key); + if (null == value || JSONObject.NULL == value) { + getLogger().warn("No field for key {}", key); + return null; + } + for (String k: value.keySet().toArray(new String[value.keySet().size()])) { + map.put(k, this.readString(value, k)); + } + return map; + } /** * Parse through the expected keys to determine whether any of the keys in From 73adbdefd225fa035f46aa3faa79138d30e13854 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 30 Mar 2016 21:43:34 +0200 Subject: [PATCH 8/8] Add test. --- .../b2/response/B2ListFilesResponseTest.java | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 src/test/java/synapticloop/b2/response/B2ListFilesResponseTest.java diff --git a/src/test/java/synapticloop/b2/response/B2ListFilesResponseTest.java b/src/test/java/synapticloop/b2/response/B2ListFilesResponseTest.java new file mode 100644 index 0000000..b5142c9 --- /dev/null +++ b/src/test/java/synapticloop/b2/response/B2ListFilesResponseTest.java @@ -0,0 +1,107 @@ +package synapticloop.b2.response; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class B2ListFilesResponseTest { + + @Test + public void testGetFiles() throws Exception { + final String json = "{\n" + + " \"files\": [\n" + + " {\n" + + " \"action\": \"upload\",\n" + + " \"contentSha1\": \"e73f8339c3e731e3fd9b0bec46222bd0016f1afa\",\n" + + " \"contentType\": \"image/jpeg\",\n" + + " \"fileId\": \"4_z181632c04c2ddde855010b15_f112de56cdcbb09e8_d20160120_m172133_c000_v0001010_t0006\",\n" + + " \"fileInfo\": {\n" + + " \"src_last_modified_millis\": \"0\"\n" + + " },\n" + + " \"fileName\": \"IMG_5066.jpg\",\n" + + " \"size\": 180903,\n" + + " \"uploadTimestamp\": 1453310493000\n" + + " },\n" + + " {\n" + + " \"action\": \"upload\",\n" + + " \"contentSha1\": \"89d2b373a7b26dbec209fe2db5c0ca6557cb1a8d\",\n" + + " \"contentType\": \"video/mp4\",\n" + + " \"fileId\": \"4_z181632c04c2ddde855010b15_f1092070825b2ec14_d20151219_m191222_c000_v0001014_t0038\",\n" + + " \"fileInfo\": {},\n" + + " \"fileName\": \"adele/BBC.Music.Presents.Adele.at.the.BBC.2015.HDTV.x264-NoGRP.mp4\",\n" + + " \"size\": 536107168,\n" + + " \"uploadTimestamp\": 1450552342000\n" + + " },\n" + + " {\n" + + " \"action\": \"upload\",\n" + + " \"contentSha1\": \"7201a995b6841a84fe23086b33d50e49a627fabe\",\n" + + " \"contentType\": \"application/octet-stream\",\n" + + " \"fileId\": \"4_z181632c04c2ddde855010b15_f109c8fe9ff88885f_d20160106_m005102_c000_v0001014_t0041\",\n" + + " \"fileInfo\": {},\n" + + " \"fileName\": \"b2sync.tar.gz\",\n" + + " \"size\": 1515462,\n" + + " \"uploadTimestamp\": 1452041462000\n" + + " },\n" + + " {\n" + + " \"action\": \"upload\",\n" + + " \"contentSha1\": \"0a9d332d09376d28cf04726e146aeedc546b09cf\",\n" + + " \"contentType\": \"image/png\",\n" + + " \"fileId\": \"4_z181632c04c2ddde855010b15_f10695c6a45107f47_d20151227_m200935_c000_v0001014_t0026\",\n" + + " \"fileInfo\": {},\n" + + " \"fileName\": \"logo/selligy-icon-square_360.png\",\n" + + " \"size\": 7583,\n" + + " \"uploadTimestamp\": 1451246975000\n" + + " },\n" + + " {\n" + + " \"action\": \"upload\",\n" + + " \"contentSha1\": \"f2cf229c6657ca2b17afb9af22090c92cf9a7d2f\",\n" + + " \"contentType\": \"application/octet-stream\",\n" + + " \"fileId\": \"4_z181632c04c2ddde855010b15_f1189dc406f02d4d9_d20160104_m040533_c000_v0001014_t0004\",\n" + + " \"fileInfo\": {},\n" + + " \"fileName\": \"uploads-large.tar.gz\",\n" + + " \"size\": 236324399,\n" + + " \"uploadTimestamp\": 1451880333000\n" + + " },\n" + + " {\n" + + " \"action\": \"upload\",\n" + + " \"contentSha1\": \"7d9133df91610fd610d817b316db01c68b988dfd\",\n" + + " \"contentType\": \"application/octet-stream\",\n" + + " \"fileId\": \"4_z181632c04c2ddde855010b15_f1189dc406f02d4d7_d20160104_m040413_c000_v0001014_t0004\",\n" + + " \"fileInfo\": {},\n" + + " \"fileName\": \"uploads-small.tar.gz\",\n" + + " \"size\": 1216666,\n" + + " \"uploadTimestamp\": 1451880253000\n" + + " },\n" + + " {\n" + + " \"action\": \"upload\",\n" + + " \"contentSha1\": \"07ecb3b3f48025118b314f5c7e169b0ca96bc3f1\",\n" + + " \"contentType\": \"application/octet-stream\",\n" + + " \"fileId\": \"4_z181632c04c2ddde855010b15_f1189dc406f02d4d8_d20160104_m040432_c000_v0001014_t0004\",\n" + + " \"fileInfo\": {},\n" + + " \"fileName\": \"uploads-smallmedium.tar.gz\",\n" + + " \"size\": 40941493,\n" + + " \"uploadTimestamp\": 1451880272000\n" + + " },\n" + + " {\n" + + " \"action\": \"upload\",\n" + + " \"contentSha1\": \"b63f91f826e87887f4ce5c28ee6988cab8a0f3f4\",\n" + + " \"contentType\": \"application/octet-stream\",\n" + + " \"fileId\": \"4_z181632c04c2ddde855010b15_f1189dc406f02d4db_d20160104_m043616_c000_v0001014_t0004\",\n" + + " \"fileInfo\": {},\n" + + " \"fileName\": \"uploads-tiny.tar.gz\",\n" + + " \"size\": 1130,\n" + + " \"uploadTimestamp\": 1451882176000\n" + + " }\n" + + " ],\n" + + " \"nextFileId\": null,\n" + + " \"nextFileName\": null\n" + + "}"; + final B2ListFilesResponse response = new B2ListFilesResponse(json); + assertNotNull(response); + assertNotNull(response.getFiles()); + assertFalse(response.getFiles().isEmpty()); + assertEquals(8, response.getFiles().size()); + assertNull(response.getNextFileId()); + assertNull(response.getNextFileName()); + } +} \ No newline at end of file