Skip to content

Commit

Permalink
feat: add support for multipart downloading of a translated file to F…
Browse files Browse the repository at this point in the history
…iles API (#104)

feat: add support for multipart downloading of a translated file to File API
  • Loading branch information
dnetrebenko-smartling authored Aug 15, 2024
1 parent f3e4658 commit 1d08d6c
Show file tree
Hide file tree
Showing 8 changed files with 748 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<FilesApi>
{
Expand All @@ -28,13 +31,18 @@ protected Class<FilesApi> 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();

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
}
Original file line number Diff line number Diff line change
@@ -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<String, String> fileHeaders;
@Getter
MediaType fileMediaType;
@Getter
TranslatedFileMetadata translatedFileMetadata;
MultipartInput multipartInput;

public void close()
{
IOUtils.closeQuietly(fileBody);
if (null != multipartInput)
multipartInput.close();
}
}
Original file line number Diff line number Diff line change
@@ -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<TranslatedFileMultipart>
{
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<TranslatedFileMultipart> clazz, Type type, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, String> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
{
Expand Down
Loading

0 comments on commit 1d08d6c

Please sign in to comment.