diff --git a/smartling-files-api/src/main/java/com/smartling/api/files/v2/FilesApi.java b/smartling-files-api/src/main/java/com/smartling/api/files/v2/FilesApi.java index cefd43cf..28b6ac32 100644 --- a/smartling-files-api/src/main/java/com/smartling/api/files/v2/FilesApi.java +++ b/smartling-files-api/src/main/java/com/smartling/api/files/v2/FilesApi.java @@ -20,6 +20,7 @@ import com.smartling.api.v2.client.exception.server.DetailedErrorMessage; import com.smartling.api.v2.response.EmptyData; import com.smartling.api.v2.response.ListResponse; +import com.smartling.api.files.v2.resteasy.ext.TranslatedFileMultipart; import org.jboss.resteasy.annotations.providers.multipart.MultipartForm; import javax.ws.rs.BeanParam; @@ -36,6 +37,7 @@ import static javax.ws.rs.core.MediaType.APPLICATION_JSON; import static javax.ws.rs.core.MediaType.MULTIPART_FORM_DATA; import static javax.ws.rs.core.MediaType.WILDCARD; +import static org.jboss.resteasy.plugins.providers.multipart.MultipartConstants.MULTIPART_MIXED; @Produces(APPLICATION_JSON) @Consumes(APPLICATION_JSON) @@ -66,6 +68,11 @@ public interface FilesApi @Produces(WILDCARD) InputStream downloadTranslatedFile(@PathParam("projectId") String projectId, @PathParam("localeId") String localeId, @BeanParam DownloadTranslationPTO downloadTranslationPTO); + @GET + @Path("/projects/{projectId}/locales/{localeId}/file") + @Produces(MULTIPART_MIXED) + TranslatedFileMultipart downloadTranslatedFileMultipart(@PathParam("projectId") String projectId, @PathParam("localeId") String localeId, @BeanParam DownloadTranslationPTO downloadTranslationPTO); + @GET @Path("/projects/{projectId}/locales/all/file/zip") @Produces(WILDCARD) diff --git a/smartling-files-api/src/main/java/com/smartling/api/files/v2/FilesApiFactory.java b/smartling-files-api/src/main/java/com/smartling/api/files/v2/FilesApiFactory.java index a870dc23..ed376365 100644 --- a/smartling-files-api/src/main/java/com/smartling/api/files/v2/FilesApiFactory.java +++ b/smartling-files-api/src/main/java/com/smartling/api/files/v2/FilesApiFactory.java @@ -1,11 +1,14 @@ package com.smartling.api.files.v2; import com.smartling.api.files.v2.exceptions.FilesApiExceptionMapper; +import com.smartling.api.files.v2.resteasy.ext.TranslatedFileMultipartReader; import com.smartling.api.v2.client.AbstractApiFactory; import com.smartling.api.v2.client.ClientConfiguration; import com.smartling.api.v2.client.ClientFactory; import com.smartling.api.v2.client.DefaultClientConfiguration; import com.smartling.api.v2.client.auth.AuthorizationRequestFilter; +import org.jboss.resteasy.client.jaxrs.internal.ResteasyClientBuilderImpl; +import org.jboss.resteasy.spi.ResteasyProviderFactory; public class FilesApiFactory extends AbstractApiFactory { @@ -28,13 +31,18 @@ protected Class getApiClass() @Override public FilesApi buildApi(final AuthorizationRequestFilter authFilter, ClientConfiguration config) { + ResteasyProviderFactory resteasyProviderFactory = config.getResteasyProviderFactory(); + if (null == resteasyProviderFactory) + resteasyProviderFactory = new ResteasyClientBuilderImpl().getProviderFactory(); + resteasyProviderFactory.register(TranslatedFileMultipartReader.class); + ClientConfiguration filesConfig = DefaultClientConfiguration.builder() .baseUrl(config.getBaseUrl()) .clientRequestFilters(config.getClientRequestFilters()) .clientResponseFilters(config.getClientResponseFilters()) .libNameVersionHolder(config.getLibNameVersionHolder()) .httpClientConfiguration(config.getHttpClientConfiguration()) - .resteasyProviderFactory(config.getResteasyProviderFactory()) + .resteasyProviderFactory(resteasyProviderFactory) .exceptionMapper(new FilesApiExceptionMapper()) .build(); diff --git a/smartling-files-api/src/main/java/com/smartling/api/files/v2/resteasy/ext/TranslatedFileMetadata.java b/smartling-files-api/src/main/java/com/smartling/api/files/v2/resteasy/ext/TranslatedFileMetadata.java new file mode 100644 index 00000000..a9d2954d --- /dev/null +++ b/smartling-files-api/src/main/java/com/smartling/api/files/v2/resteasy/ext/TranslatedFileMetadata.java @@ -0,0 +1,10 @@ +package com.smartling.api.files.v2.resteasy.ext; + +import lombok.Data; + +@Data +public class TranslatedFileMetadata +{ + // Now we leave the class empty and will use it as an end marker for the file body only. + // In the future we will expand this object with additional metadata associated with the downloaded file. +} diff --git a/smartling-files-api/src/main/java/com/smartling/api/files/v2/resteasy/ext/TranslatedFileMultipart.java b/smartling-files-api/src/main/java/com/smartling/api/files/v2/resteasy/ext/TranslatedFileMultipart.java new file mode 100644 index 00000000..a2c36c92 --- /dev/null +++ b/smartling-files-api/src/main/java/com/smartling/api/files/v2/resteasy/ext/TranslatedFileMultipart.java @@ -0,0 +1,29 @@ +package com.smartling.api.files.v2.resteasy.ext; + +import lombok.Getter; +import org.apache.commons.io.IOUtils; +import org.jboss.resteasy.plugins.providers.multipart.MultipartInput; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import java.io.InputStream; + +public class TranslatedFileMultipart +{ + @Getter + InputStream fileBody; + @Getter + MultivaluedMap fileHeaders; + @Getter + MediaType fileMediaType; + @Getter + TranslatedFileMetadata translatedFileMetadata; + MultipartInput multipartInput; + + public void close() + { + IOUtils.closeQuietly(fileBody); + if (null != multipartInput) + multipartInput.close(); + } +} diff --git a/smartling-files-api/src/main/java/com/smartling/api/files/v2/resteasy/ext/TranslatedFileMultipartReader.java b/smartling-files-api/src/main/java/com/smartling/api/files/v2/resteasy/ext/TranslatedFileMultipartReader.java new file mode 100644 index 00000000..2420c504 --- /dev/null +++ b/smartling-files-api/src/main/java/com/smartling/api/files/v2/resteasy/ext/TranslatedFileMultipartReader.java @@ -0,0 +1,72 @@ +package com.smartling.api.files.v2.resteasy.ext; + +import org.apache.commons.lang3.StringUtils; +import org.jboss.resteasy.plugins.providers.multipart.InputPart; +import org.jboss.resteasy.plugins.providers.multipart.MultipartInputImpl; +import org.jboss.resteasy.plugins.providers.multipart.i18n.Messages; + +import javax.ws.rs.Consumes; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.Provider; +import javax.ws.rs.ext.Providers; +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import static java.lang.String.format; +import static org.jboss.resteasy.plugins.providers.multipart.MultipartConstants.MULTIPART_MIXED; + +@Provider +@Consumes(MULTIPART_MIXED) +public class TranslatedFileMultipartReader implements MessageBodyReader +{ + private static final String BOUNDARY_PARAMETER = "boundary"; + + protected @Context Providers workers; + + @Override + public boolean isReadable(Class clazz, Type type, Annotation[] annotations, MediaType mediaType) + { + return type.equals(TranslatedFileMultipart.class); + } + + @Override + public TranslatedFileMultipart readFrom(Class clazz, Type type, Annotation[] annotations, MediaType mediaType, MultivaluedMap multivaluedMap, InputStream inputStream) throws IOException, WebApplicationException + { + final String boundary = mediaType.getParameters().get(BOUNDARY_PARAMETER); + if (null == boundary) + throw new IOException(Messages.MESSAGES.unableToGetBoundary()); + final MultipartInputImpl multipartInput = new MultipartInputImpl(mediaType, workers); + multipartInput.parse(inputStream); + + if (multipartInput.getParts().size() != 2) + throw new IOException(format("The response contains unexpected number of parts (%d). Two parts are expected. The first one is file's body, the second one is metadata object.", multipartInput.getParts().size())); + + final InputPart filePart = multipartInput.getParts().get(0); + final InputPart metadataPart = multipartInput.getParts().get(1); + + final TranslatedFileMultipart translatedFileMultipart = new TranslatedFileMultipart(); + translatedFileMultipart.multipartInput = multipartInput; + translatedFileMultipart.fileBody = filePart.getBody(InputStream.class, null); + translatedFileMultipart.fileHeaders = filePart.getHeaders(); + translatedFileMultipart.fileMediaType = filePart.getMediaType(); + + final MediaType metadataPartMediaType = metadataPart.getMediaType(); + if (null == metadataPartMediaType) + throw new IOException("Missing content type of metadata part."); + + if (!StringUtils.equalsIgnoreCase(MediaType.APPLICATION_JSON_TYPE.getType(), metadataPartMediaType.getType()) || !StringUtils.equalsIgnoreCase(MediaType.APPLICATION_JSON_TYPE.getSubtype(), metadataPartMediaType.getSubtype())) + throw new IOException(format("Unexpected content type of metadata part (%s). Expected (%s) metadata part.", metadataPart.getMediaType(), MediaType.APPLICATION_JSON_TYPE)); + + translatedFileMultipart.translatedFileMetadata = metadataPart.getBody(TranslatedFileMetadata.class, TranslatedFileMetadata.class); + if (null == translatedFileMultipart.translatedFileMetadata) + throw new IOException("Cannot construct metadata json object."); + + return translatedFileMultipart; + } +} diff --git a/smartling-files-api/src/test/java/com/smartling/api/files/v2/FilesApiIntTest.java b/smartling-files-api/src/test/java/com/smartling/api/files/v2/FilesApiIntTest.java index cce6933b..8ddd9e81 100644 --- a/smartling-files-api/src/test/java/com/smartling/api/files/v2/FilesApiIntTest.java +++ b/smartling-files-api/src/test/java/com/smartling/api/files/v2/FilesApiIntTest.java @@ -7,6 +7,7 @@ import com.smartling.api.files.v2.pto.GetFileLastModifiedPTO; import com.smartling.api.files.v2.pto.UploadFilePTO; import com.smartling.api.files.v2.pto.UploadFileResponse; +import com.smartling.api.files.v2.resteasy.ext.TranslatedFileMultipart; import com.smartling.api.v2.client.ClientConfiguration; import com.smartling.api.v2.client.DefaultClientConfiguration; import com.smartling.api.v2.client.auth.BearerAuthStaticTokenFilter; @@ -87,6 +88,44 @@ public void shouldRetrieveTranslation() throws Exception assertEquals(IOUtils.toString(response, UTF_8), rawBody); } + @Test + public void shouldRetrieveTranslationMultipart() throws Exception + { + final String boundary = UUID.randomUUID().toString(); + final String responseBody = "--" + boundary + "\r\n" + + "Content-Type: application/octet-stream; charset=UTF-8\r\n" + + "Content-Disposition: attachment; filename=\"myfile.properties\";\r\n" + + "\r\n" + + "key1=value1\nkey2=value2\r\n" + + "--" + boundary + "\r\n" + + "Content-Type: application/json; charset=UTF-8\r\n" + + "\r\n" + + "{\"key\":\"value\"}\r\n" + + "--" + boundary + "--"; + + smartlingApi.stubFor(get(urlPathMatching("/files-api/v2/projects/.+/locales/zh-CN/file.*")) + .withQueryParam("fileUri", equalTo(FILE_URI)) + .withQueryParam("retrievalType", equalTo(PUBLISHED.toString())) + .willReturn(aResponse() + .withHeader("Content-Type", "multipart/mixed; boundary=" + boundary) + .withBody(responseBody) + ) + ); + + TranslatedFileMultipart response = filesApi.downloadTranslatedFileMultipart( + PROJECT_ID, + "zh-CN", + DownloadTranslationPTO.builder() + .fileUri(FILE_URI) + .retrievalType(PUBLISHED) + .build() + ); + + InputStream stream = response.getFileBody(); + + assertEquals(IOUtils.toString(stream, UTF_8), "key1=value1\nkey2=value2"); + } + @Test public void shouldUploadFile() throws Exception { diff --git a/smartling-files-api/src/test/java/com/smartling/api/files/v2/FilesApiTest.java b/smartling-files-api/src/test/java/com/smartling/api/files/v2/FilesApiTest.java index 2cb1f989..ddc5b0fd 100644 --- a/smartling-files-api/src/test/java/com/smartling/api/files/v2/FilesApiTest.java +++ b/smartling-files-api/src/test/java/com/smartling/api/files/v2/FilesApiTest.java @@ -1,14 +1,17 @@ package com.smartling.api.files.v2; import com.github.tomakehurst.wiremock.junit.WireMockRule; +import com.smartling.api.files.v2.pto.DownloadTranslationPTO; import com.smartling.api.files.v2.pto.FileItemPTO; import com.smartling.api.files.v2.pto.FileLocaleStatusResponse; import com.smartling.api.files.v2.pto.FileStatusResponse; import com.smartling.api.files.v2.pto.FileType; import com.smartling.api.files.v2.pto.GetFilesListPTO; import com.smartling.api.files.v2.pto.OrderBy; +import com.smartling.api.files.v2.pto.RetrievalType; import com.smartling.api.files.v2.pto.UploadFilePTO; import com.smartling.api.files.v2.pto.UploadFileResponse; +import com.smartling.api.files.v2.resteasy.ext.TranslatedFileMultipart; import com.smartling.api.v2.client.ClientConfiguration; import com.smartling.api.v2.client.DefaultClientConfiguration; import com.smartling.api.v2.client.auth.BearerAuthStaticTokenFilter; @@ -17,14 +20,20 @@ import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; +import org.apache.commons.io.IOUtils; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; import java.io.ByteArrayInputStream; +import java.io.InputStream; import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Arrays; @@ -32,8 +41,11 @@ import java.util.HashMap; import java.util.Locale; import java.util.Map; +import java.util.UUID; import static com.smartling.api.files.v2.pto.FileType.JSON; +import static java.util.Collections.singletonList; +import static org.jboss.resteasy.plugins.providers.multipart.MultipartConstants.MULTIPART_MIXED; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -314,6 +326,272 @@ public void testUploadFile() throws Exception assertEquals("POST", recordedRequest.getMethod()); } + @Test + public void testDownloadTranslatedFileMultipartSuccess() throws Exception + { + final String boundary = UUID.randomUUID().toString(); + final String responseBody = "--" + boundary + "\r\n" + + "Content-Type: application/octet-stream; charset=UTF-8\r\n" + + "Content-Disposition: attachment; filename=\"myfile.properties\";\r\n" + + "\r\n" + + "key1=value1\nkey2=value2\r\n" + + "--" + boundary + "\r\n" + + "Content-Type: application/json; charset=UTF-8\r\n" + + "\r\n" + + "{\"key\":\"value\"}\r\n" + + "--" + boundary + "--"; + + final MockResponse response = new MockResponse() + .setResponseCode(200) + .setHeader(HttpHeaders.CONTENT_TYPE, MULTIPART_MIXED + "; boundary=" + boundary) + .setBody(responseBody); + mockWebServer.enqueue(response); + + final TranslatedFileMultipart translatedFileMultipart = filesApi.downloadTranslatedFileMultipart( + PROJECT_ID, + "es-ES", + DownloadTranslationPTO.builder() + .fileUri("myfile.properties") + .retrievalType(RetrievalType.PENDING) + .includeOriginalStrings(true) + .build() + ); + + assertNotNull(translatedFileMultipart); + assertNotNull(translatedFileMultipart.getTranslatedFileMetadata()); + + final MultivaluedMap headers = new MultivaluedHashMap<>(); + headers.put(HttpHeaders.CONTENT_TYPE, singletonList("application/octet-stream; charset=UTF-8")); + headers.put(HttpHeaders.CONTENT_DISPOSITION, singletonList("attachment; filename=\"myfile.properties\";")); + assertEquals(headers, translatedFileMultipart.getFileHeaders()); + assertEquals(MediaType.APPLICATION_OCTET_STREAM_TYPE.withCharset("UTF-8"), translatedFileMultipart.getFileMediaType()); + final InputStream inputStream = translatedFileMultipart.getFileBody(); + final String fileString = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + assertEquals("key1=value1\nkey2=value2", fileString); + + final RecordedRequest recordedRequest = mockWebServer.takeRequest(); + assertEquals("GET", recordedRequest.getMethod()); + } + + @Test(expected = RestApiRuntimeException.class) + public void testDownloadTranslatedFileMultipartWhenFailedDueToBoundaryAbsence() + { + final String boundary = UUID.randomUUID().toString(); + final String responseBody = "--" + boundary + "\r\n" + + "Content-Type: application/octet-stream; charset=UTF-8\r\n" + + "Content-Disposition: attachment; filename=\"myfile.properties\";\r\n" + + "\r\n" + + "key1=value1\nkey2=value2\r\n" + + "--" + boundary + "\r\n" + + "Content-Type: application/json; charset=UTF-8\r\n" + + "\r\n" + + "{\"key\":\"value\"}\r\n" + + "--" + boundary + "--"; + + final MockResponse response = new MockResponse() + .setResponseCode(200) + .setHeader(HttpHeaders.CONTENT_TYPE, MULTIPART_MIXED) + .setBody(responseBody); + mockWebServer.enqueue(response); + + filesApi.downloadTranslatedFileMultipart( + PROJECT_ID, + "es-ES", + DownloadTranslationPTO.builder() + .fileUri("myfile.properties") + .retrievalType(RetrievalType.PENDING) + .includeOriginalStrings(true) + .build() + ); + } + + @Test(expected = RestApiRuntimeException.class) + public void testDownloadTranslatedFileMultipartWhenFailedDueToBigNumberOfParts() + { + final String boundary = UUID.randomUUID().toString(); + final String responseBody = "--" + boundary + "\r\n" + + "Content-Type: application/octet-stream; charset=UTF-8\r\n" + + "Content-Disposition: attachment; filename=\"myfile.properties\";\r\n" + + "\r\n" + + "key1=value1\nkey2=value2\r\n" + + "--" + boundary + "\r\n" + + "Content-Type: application/json; charset=UTF-8\r\n" + + "\r\n" + + "{\"key\":\"value\"}\r\n" + + "--" + boundary + "\r\n" + + "Content-Type: application/json; charset=UTF-8\r\n" + + "\r\n" + + "{\"key\":\"value\"}\r\n" + + "--" + boundary + "--"; + + final MockResponse response = new MockResponse() + .setResponseCode(200) + .setHeader(HttpHeaders.CONTENT_TYPE, MULTIPART_MIXED + "; boundary=" + boundary) + .setBody(responseBody); + mockWebServer.enqueue(response); + + filesApi.downloadTranslatedFileMultipart( + PROJECT_ID, + "es-ES", + DownloadTranslationPTO.builder() + .fileUri("myfile.properties") + .retrievalType(RetrievalType.PENDING) + .includeOriginalStrings(true) + .build() + ); + } + + @Test(expected = RestApiRuntimeException.class) + public void testDownloadTranslatedFileMultipartWhenFailedDueToSmallNumberOfParts() + { + final String boundary = UUID.randomUUID().toString(); + final String responseBody = "--" + boundary + "\r\n" + + "Content-Type: application/octet-stream; charset=UTF-8\r\n" + + "Content-Disposition: attachment; filename=\"myfile.properties\";\r\n" + + "\r\n" + + "key1=value1\nkey2=value2\r\n" + + "--" + boundary + "--"; + + final MockResponse response = new MockResponse() + .setResponseCode(200) + .setHeader(HttpHeaders.CONTENT_TYPE, MULTIPART_MIXED + "; boundary=" + boundary) + .setBody(responseBody); + mockWebServer.enqueue(response); + + filesApi.downloadTranslatedFileMultipart( + PROJECT_ID, + "es-ES", + DownloadTranslationPTO.builder() + .fileUri("myfile.properties") + .retrievalType(RetrievalType.PENDING) + .includeOriginalStrings(true) + .build() + ); + } + + @Test(expected = RestApiRuntimeException.class) + public void testDownloadTranslatedFileMultipartWhenFailedDueToInvalidMetadataContentType() + { + final String boundary = UUID.randomUUID().toString(); + final String responseBody = "--" + boundary + "\r\n" + + "Content-Type: application/octet-stream; charset=UTF-8\r\n" + + "Content-Disposition: attachment; filename=\"myfile.properties\";\r\n" + + "\r\n" + + "key1=value1\nkey2=value2\r\n" + + "--" + boundary + "\r\n" + + "Content-Type: application/octet-stream; charset=UTF-8\r\n" + + "\r\n" + + "{\"key\":\"value\"}\r\n" + + "--" + boundary + "--"; + + final MockResponse response = new MockResponse() + .setResponseCode(200) + .setHeader(HttpHeaders.CONTENT_TYPE, MULTIPART_MIXED + "; boundary=" + boundary) + .setBody(responseBody); + mockWebServer.enqueue(response); + + filesApi.downloadTranslatedFileMultipart( + PROJECT_ID, + "es-ES", + DownloadTranslationPTO.builder() + .fileUri("myfile.properties") + .retrievalType(RetrievalType.PENDING) + .includeOriginalStrings(true) + .build() + ); + } + + @Test(expected = RestApiRuntimeException.class) + public void testDownloadTranslatedFileMultipartWhenFailedDueToAbsenceOfMetadataJson() + { + final String boundary = UUID.randomUUID().toString(); + final String responseBody = "--" + boundary + "\r\n" + + "Content-Type: application/octet-stream; charset=UTF-8\r\n" + + "Content-Disposition: attachment; filename=\"myfile.properties\";\r\n" + + "\r\n" + + "key1=value1\nkey2=value2\r\n" + + "--" + boundary + "\r\n" + + "Content-Type: application/json; charset=UTF-8\r\n" + + "\r\n" + + "\r\n" + + "--" + boundary + "--"; + + final MockResponse response = new MockResponse() + .setResponseCode(200) + .setHeader(HttpHeaders.CONTENT_TYPE, MULTIPART_MIXED + "; boundary=" + boundary) + .setBody(responseBody); + mockWebServer.enqueue(response); + + filesApi.downloadTranslatedFileMultipart( + PROJECT_ID, + "es-ES", + DownloadTranslationPTO.builder() + .fileUri("myfile.properties") + .retrievalType(RetrievalType.PENDING) + .includeOriginalStrings(true) + .build() + ); + } + + @Test(expected = RestApiRuntimeException.class) + public void testDownloadTranslatedFileMultipartWhenFailedDueToInvalidMetadataJson() + { + final String boundary = UUID.randomUUID().toString(); + final String responseBody = "--" + boundary + "\r\n" + + "Content-Type: application/octet-stream; charset=UTF-8\r\n" + + "Content-Disposition: attachment; filename=\"myfile.properties\";\r\n" + + "\r\n" + + "key1=value1\nkey2=value2\r\n" + + "--" + boundary + "\r\n" + + "Content-Type: application/json; charset=UTF-8\r\n" + + "{\"key\r\n" + + "\r\n" + + "--" + boundary + "--"; + + final MockResponse response = new MockResponse() + .setResponseCode(200) + .setHeader(HttpHeaders.CONTENT_TYPE, MULTIPART_MIXED + "; boundary=" + boundary) + .setBody(responseBody); + mockWebServer.enqueue(response); + + filesApi.downloadTranslatedFileMultipart( + PROJECT_ID, + "es-ES", + DownloadTranslationPTO.builder() + .fileUri("myfile.properties") + .retrievalType(RetrievalType.PENDING) + .includeOriginalStrings(true) + .build() + ); + } + + @Test(expected = RestApiRuntimeException.class) + public void testDownloadTranslatedFileMultipartWhenFailedDueToBrokenResponse() throws Exception + { + final String boundary = UUID.randomUUID().toString(); + final String responseBody = "--" + boundary + "\r\n" + + "Content-Type: application/octet-stream; charset=UTF-8\r\n" + + "Content-Disposition: attachment; filename=\"myfile.properties\";\r\n" + + "\r\n" + + "key1=value1\nkey2="; + + final MockResponse response = new MockResponse() + .setResponseCode(200) + .setHeader(HttpHeaders.CONTENT_TYPE, MULTIPART_MIXED + "; boundary=" + boundary) + .setBody(responseBody); + mockWebServer.enqueue(response); + + filesApi.downloadTranslatedFileMultipart( + PROJECT_ID, + "es-ES", + DownloadTranslationPTO.builder() + .fileUri("myfile.properties") + .retrievalType(RetrievalType.PENDING) + .includeOriginalStrings(true) + .build() + ); + } + private static Date date(String date) throws ParseException { return FAPI_DATE_FORMAT.parse(date); diff --git a/smartling-files-api/src/test/java/com/smartling/api/files/v2/resteasy/ext/TranslatedFileMultipartReaderTest.java b/smartling-files-api/src/test/java/com/smartling/api/files/v2/resteasy/ext/TranslatedFileMultipartReaderTest.java new file mode 100644 index 00000000..577c44ca --- /dev/null +++ b/smartling-files-api/src/test/java/com/smartling/api/files/v2/resteasy/ext/TranslatedFileMultipartReaderTest.java @@ -0,0 +1,304 @@ +package com.smartling.api.files.v2.resteasy.ext; + +import org.apache.commons.io.IOUtils; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.Providers; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +import static java.util.Collections.singletonList; +import static org.jboss.resteasy.plugins.providers.multipart.MultipartConstants.MULTIPART_MIXED; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +public class TranslatedFileMultipartReaderTest +{ + @InjectMocks + private TranslatedFileMultipartReader translatedFileMultipartReader = new TranslatedFileMultipartReader(); + + @Mock + protected Providers workers; + + @Before + public void setUp() + { + initMocks(this); + } + + @Test + public void testIsReadable() + { + assertTrue(translatedFileMultipartReader.isReadable(TranslatedFileMultipart.class, TranslatedFileMultipart.class, null, null)); + assertFalse(translatedFileMultipartReader.isReadable(TranslatedFileMultipart.class, Integer.class, null, null)); + } + + @Test + public void testReadFromWhenSuccess() throws IOException + { + final String boundary = UUID.randomUUID().toString(); + final String responseBody = "--" + boundary + "\r\n" + + "Content-Type: application/octet-stream; charset=UTF-8\r\n" + + "Content-Disposition: attachment; filename=\"myfile.properties\";\r\n" + + "\r\n" + + "key1=value1\nkey2=value2\r\n" + + "--" + boundary + "\r\n" + + "Content-Type: application/json; charset=UTF-8\r\n" + + "\r\n" + + "{\"key\":\"value\"}\r\n" + + "--" + boundary + "--"; + + final MediaType fileMediaType = MediaType.valueOf("application/octet-stream; charset=UTF-8"); + + final MessageBodyReader inputStreamReader = mock(MessageBodyReader.class); + when(inputStreamReader.isReadable(InputStream.class, null, null, fileMediaType)).thenReturn(true); + when(inputStreamReader.readFrom(any(), any(), any(), any(), any(), any())).thenReturn(new ByteArrayInputStream("key1=value1\nkey2=value2".getBytes())); + when(workers.getMessageBodyReader(InputStream.class, null, new Annotation[] {}, fileMediaType)).thenReturn(inputStreamReader); + + final MediaType metadataMediaType = MediaType.valueOf("application/json; charset=UTF-8"); + + final TranslatedFileMetadata translatedFileMetadata = mock(TranslatedFileMetadata.class); + final MessageBodyReader metadataReader = mock(MessageBodyReader.class); + when(metadataReader.isReadable(TranslatedFileMetadata.class, TranslatedFileMetadata.class, null, metadataMediaType)).thenReturn(true); + when(metadataReader.readFrom(any(), any(), any(), any(), any(), any())).thenReturn(translatedFileMetadata); + when(workers.getMessageBodyReader(TranslatedFileMetadata.class, TranslatedFileMetadata.class, new Annotation[] {}, metadataMediaType)).thenReturn(metadataReader); + + final TranslatedFileMultipart translatedFileMultipart = translatedFileMultipartReader.readFrom( + TranslatedFileMultipart.class, + TranslatedFileMultipart.class, + null, + MediaType.valueOf(MULTIPART_MIXED + "; boundary=" + boundary), + null, + new ByteArrayInputStream(responseBody.getBytes()) + ); + assertNotNull(translatedFileMultipart); + assertSame(translatedFileMetadata, translatedFileMultipart.getTranslatedFileMetadata()); + + final MultivaluedMap headers = new MultivaluedHashMap<>(); + headers.put(HttpHeaders.CONTENT_TYPE, singletonList("application/octet-stream; charset=UTF-8")); + headers.put(HttpHeaders.CONTENT_DISPOSITION, singletonList("attachment; filename=\"myfile.properties\";")); + assertEquals(headers, translatedFileMultipart.getFileHeaders()); + assertEquals(MediaType.APPLICATION_OCTET_STREAM_TYPE.withCharset("UTF-8"), translatedFileMultipart.getFileMediaType()); + final InputStream inputStream = translatedFileMultipart.getFileBody(); + final String fileString = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + assertEquals("key1=value1\nkey2=value2", fileString); + } + + @Test(expected = IOException.class) + public void testReadFromWhenFailedDueToBoundaryAbsence() throws IOException + { + final String boundary = UUID.randomUUID().toString(); + final String responseBody = "--" + boundary + "\r\n" + + "Content-Type: application/octet-stream; charset=UTF-8\r\n" + + "Content-Disposition: attachment; filename=\"myfile.properties\";\r\n" + + "\r\n" + + "key1=value1\nkey2=value2\r\n" + + "--" + boundary + "\r\n" + + "Content-Type: application/json; charset=UTF-8\r\n" + + "\r\n" + + "{\"key\":\"value\"}\r\n" + + "--" + boundary + "--"; + + final MediaType fileMediaType = MediaType.valueOf("application/octet-stream; charset=UTF-8"); + + final MessageBodyReader inputStreamReader = mock(MessageBodyReader.class); + when(inputStreamReader.isReadable(InputStream.class, null, null, fileMediaType)).thenReturn(true); + when(inputStreamReader.readFrom(any(), any(), any(), any(), any(), any())).thenReturn(new ByteArrayInputStream("key1=value1\nkey2=value2".getBytes())); + when(workers.getMessageBodyReader(InputStream.class, null, new Annotation[] {}, fileMediaType)).thenReturn(inputStreamReader); + + final MediaType metadataMediaType = MediaType.valueOf("application/json; charset=UTF-8"); + + final TranslatedFileMetadata translatedFileMetadata = mock(TranslatedFileMetadata.class); + final MessageBodyReader metadataReader = mock(MessageBodyReader.class); + when(metadataReader.isReadable(TranslatedFileMetadata.class, TranslatedFileMetadata.class, null, metadataMediaType)).thenReturn(true); + when(metadataReader.readFrom(any(), any(), any(), any(), any(), any())).thenReturn(translatedFileMetadata); + when(workers.getMessageBodyReader(TranslatedFileMetadata.class, TranslatedFileMetadata.class, new Annotation[] {}, metadataMediaType)).thenReturn(metadataReader); + + translatedFileMultipartReader.readFrom( + TranslatedFileMultipart.class, + TranslatedFileMultipart.class, + null, + MediaType.valueOf(MULTIPART_MIXED), + null, + new ByteArrayInputStream(responseBody.getBytes()) + ); + } + + @Test(expected = IOException.class) + public void testReadFromWhenFailedDueToBigNumberOfParts() throws IOException + { + final String boundary = UUID.randomUUID().toString(); + final String responseBody = "--" + boundary + "\r\n" + + "Content-Type: application/octet-stream; charset=UTF-8\r\n" + + "Content-Disposition: attachment; filename=\"myfile.properties\";\r\n" + + "\r\n" + + "key1=value1\nkey2=value2\r\n" + + "--" + boundary + "\r\n" + + "Content-Type: application/json; charset=UTF-8\r\n" + + "\r\n" + + "{\"key\":\"value\"}\r\n" + + "--" + boundary + "\r\n" + + "Content-Type: application/json; charset=UTF-8\r\n" + + "\r\n" + + "{\"key\":\"value\"}\r\n" + + "--" + boundary + "--"; + + final MediaType fileMediaType = MediaType.valueOf("application/octet-stream; charset=UTF-8"); + + final MessageBodyReader inputStreamReader = mock(MessageBodyReader.class); + when(inputStreamReader.isReadable(InputStream.class, null, null, fileMediaType)).thenReturn(true); + when(inputStreamReader.readFrom(any(), any(), any(), any(), any(), any())).thenReturn(new ByteArrayInputStream("key1=value1\nkey2=value2".getBytes())); + when(workers.getMessageBodyReader(InputStream.class, null, new Annotation[] {}, fileMediaType)).thenReturn(inputStreamReader); + + final MediaType metadataMediaType = MediaType.valueOf("application/json; charset=UTF-8"); + + final TranslatedFileMetadata translatedFileMetadata = mock(TranslatedFileMetadata.class); + final MessageBodyReader metadataReader = mock(MessageBodyReader.class); + when(metadataReader.isReadable(TranslatedFileMetadata.class, TranslatedFileMetadata.class, null, metadataMediaType)).thenReturn(true); + when(metadataReader.readFrom(any(), any(), any(), any(), any(), any())).thenReturn(translatedFileMetadata); + when(workers.getMessageBodyReader(TranslatedFileMetadata.class, TranslatedFileMetadata.class, new Annotation[] {}, metadataMediaType)).thenReturn(metadataReader); + + translatedFileMultipartReader.readFrom( + TranslatedFileMultipart.class, + TranslatedFileMultipart.class, + null, + MediaType.valueOf(MULTIPART_MIXED + "; boundary=" + boundary), + null, + new ByteArrayInputStream(responseBody.getBytes()) + ); + } + + @Test(expected = IOException.class) + public void testReadFromWhenFailedDueToSmallNumberOfParts() throws IOException + { + final String boundary = UUID.randomUUID().toString(); + final String responseBody = "--" + boundary + "\r\n" + + "Content-Type: application/octet-stream; charset=UTF-8\r\n" + + "Content-Disposition: attachment; filename=\"myfile.properties\";\r\n" + + "\r\n" + + "key1=value1\nkey2=value2\r\n" + + "--" + boundary + "--"; + + final MediaType fileMediaType = MediaType.valueOf("application/octet-stream; charset=UTF-8"); + + final MessageBodyReader inputStreamReader = mock(MessageBodyReader.class); + when(inputStreamReader.isReadable(InputStream.class, null, null, fileMediaType)).thenReturn(true); + when(inputStreamReader.readFrom(any(), any(), any(), any(), any(), any())).thenReturn(new ByteArrayInputStream("key1=value1\nkey2=value2".getBytes())); + when(workers.getMessageBodyReader(InputStream.class, null, new Annotation[] {}, fileMediaType)).thenReturn(inputStreamReader); + + final MediaType metadataMediaType = MediaType.valueOf("application/json; charset=UTF-8"); + + final TranslatedFileMetadata translatedFileMetadata = mock(TranslatedFileMetadata.class); + final MessageBodyReader metadataReader = mock(MessageBodyReader.class); + when(metadataReader.isReadable(TranslatedFileMetadata.class, TranslatedFileMetadata.class, null, metadataMediaType)).thenReturn(true); + when(metadataReader.readFrom(any(), any(), any(), any(), any(), any())).thenReturn(translatedFileMetadata); + when(workers.getMessageBodyReader(TranslatedFileMetadata.class, TranslatedFileMetadata.class, new Annotation[] {}, metadataMediaType)).thenReturn(metadataReader); + + translatedFileMultipartReader.readFrom( + TranslatedFileMultipart.class, + TranslatedFileMultipart.class, + null, + MediaType.valueOf(MULTIPART_MIXED + "; boundary=" + boundary), + null, + new ByteArrayInputStream(responseBody.getBytes()) + ); + } + + @Test(expected = IOException.class) + public void testReadFromWhenFailedDueToInvalidMetadataContentType() throws IOException + { + final String boundary = UUID.randomUUID().toString(); + final String responseBody = "--" + boundary + "\r\n" + + "Content-Type: application/octet-stream; charset=UTF-8\r\n" + + "Content-Disposition: attachment; filename=\"myfile.properties\";\r\n" + + "\r\n" + + "key1=value1\nkey2=value2\r\n" + + "--" + boundary + "\r\n" + + "Content-Type: application/octet-stream; charset=UTF-8\r\n" + + "\r\n" + + "{\"key\":\"value\"}\r\n" + + "--" + boundary + "--"; + + final MediaType fileMediaType = MediaType.valueOf("application/octet-stream; charset=UTF-8"); + + final MessageBodyReader inputStreamReader = mock(MessageBodyReader.class); + when(inputStreamReader.isReadable(InputStream.class, null, null, fileMediaType)).thenReturn(true); + when(inputStreamReader.readFrom(any(), any(), any(), any(), any(), any())).thenReturn(new ByteArrayInputStream("key1=value1\nkey2=value2".getBytes())); + when(workers.getMessageBodyReader(InputStream.class, null, new Annotation[] {}, fileMediaType)).thenReturn(inputStreamReader); + + final MediaType metadataMediaType = MediaType.valueOf("application/octet-stream; charset=UTF-8"); + + final TranslatedFileMetadata translatedFileMetadata = mock(TranslatedFileMetadata.class); + final MessageBodyReader metadataReader = mock(MessageBodyReader.class); + when(metadataReader.isReadable(TranslatedFileMetadata.class, TranslatedFileMetadata.class, null, metadataMediaType)).thenReturn(true); + when(metadataReader.readFrom(any(), any(), any(), any(), any(), any())).thenReturn(translatedFileMetadata); + when(workers.getMessageBodyReader(TranslatedFileMetadata.class, TranslatedFileMetadata.class, new Annotation[] {}, metadataMediaType)).thenReturn(metadataReader); + + translatedFileMultipartReader.readFrom( + TranslatedFileMultipart.class, + TranslatedFileMultipart.class, + null, + MediaType.valueOf(MULTIPART_MIXED + "; boundary=" + boundary), + null, + new ByteArrayInputStream(responseBody.getBytes()) + ); + } + + @Test(expected = IOException.class) + public void testReadFromWhenFailedDueToInvalidMetadataJson() throws IOException + { + final String boundary = UUID.randomUUID().toString(); + final String responseBody = "--" + boundary + "\r\n" + + "Content-Type: application/octet-stream; charset=UTF-8\r\n" + + "Content-Disposition: attachment; filename=\"myfile.properties\";\r\n" + + "\r\n" + + "key1=value1\nkey2=value2\r\n" + + "--" + boundary + "\r\n" + + "Content-Type: application/json; charset=UTF-8\r\n" + + "\r\n" + + "{\"key\":\"value\"}\r\n" + + "--" + boundary + "--"; + + final MediaType fileMediaType = MediaType.valueOf("application/octet-stream; charset=UTF-8"); + + final MessageBodyReader inputStreamReader = mock(MessageBodyReader.class); + when(inputStreamReader.isReadable(InputStream.class, null, null, fileMediaType)).thenReturn(true); + when(inputStreamReader.readFrom(any(), any(), any(), any(), any(), any())).thenReturn(new ByteArrayInputStream("key1=value1\nkey2=value2".getBytes())); + when(workers.getMessageBodyReader(InputStream.class, null, new Annotation[] {}, fileMediaType)).thenReturn(inputStreamReader); + + final MediaType metadataMediaType = MediaType.valueOf("application/json; charset=UTF-8"); + + final MessageBodyReader metadataReader = mock(MessageBodyReader.class); + when(metadataReader.isReadable(TranslatedFileMetadata.class, TranslatedFileMetadata.class, null, metadataMediaType)).thenReturn(true); + when(metadataReader.readFrom(any(), any(), any(), any(), any(), any())).thenReturn(null); + when(workers.getMessageBodyReader(TranslatedFileMetadata.class, TranslatedFileMetadata.class, new Annotation[] {}, metadataMediaType)).thenReturn(metadataReader); + + translatedFileMultipartReader.readFrom( + TranslatedFileMultipart.class, + TranslatedFileMultipart.class, + null, + MediaType.valueOf(MULTIPART_MIXED + "; boundary=" + boundary), + null, + new ByteArrayInputStream(responseBody.getBytes()) + ); + } +}