de.mklinger.qetcher.client.impl.QetcherClientImpl Maven / Gradle / Ivy
package de.mklinger.qetcher.client.impl;
import static de.mklinger.qetcher.client.impl.Parameters.*;
import java.net.URI;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.mklinger.commons.httpclient.BodyHandlers;
import de.mklinger.commons.httpclient.BodyProviders;
import de.mklinger.commons.httpclient.HttpClient;
import de.mklinger.commons.httpclient.HttpRequest;
import de.mklinger.commons.httpclient.HttpResponse;
import de.mklinger.commons.httpclient.HttpResponse.BodyHandler;
import de.mklinger.micro.annotations.VisibleForTesting;
import de.mklinger.qetcher.client.InputConversionFile;
import de.mklinger.qetcher.client.InputJob;
import de.mklinger.qetcher.client.QetcherClientVersion;
import de.mklinger.qetcher.client.impl.lookup.ServiceUriSupplier;
import de.mklinger.qetcher.client.model.v1.AvailableConversion;
import de.mklinger.qetcher.client.model.v1.AvailableConversions;
import de.mklinger.qetcher.client.model.v1.AvailableNode;
import de.mklinger.qetcher.client.model.v1.AvailableNodes;
import de.mklinger.qetcher.client.model.v1.Builders;
import de.mklinger.qetcher.client.model.v1.ConversionFile;
import de.mklinger.qetcher.client.model.v1.ConversionFiles;
import de.mklinger.qetcher.client.model.v1.FileExtensionInfos;
import de.mklinger.qetcher.client.model.v1.Job;
import de.mklinger.qetcher.client.model.v1.JobPatch;
import de.mklinger.qetcher.client.model.v1.Jobs;
import de.mklinger.qetcher.client.model.v1.MediaTypeInfo;
import de.mklinger.qetcher.client.model.v1.MediaTypeInfos;
/**
* Qetcher client implementation that uses
* {@link de.mklinger.commons.httpclient.HttpClient mklinger httpclient}
* as underlying HTTP client implementation.
*
*
* This implementation supports asynchronous HTTP/2 with JDK8 and above.
*
*
* @author Marc Klinger - mklinger[at]mklinger[dot]de
*/
public class QetcherClientImpl extends AbstractQetcherClient {
private static final String USER_AGENT = "User-Agent";
private static final String USER_AGENT_VALUE = "qetcher-client/" + QetcherClientVersion.getVersion();
private static final Logger LOG = LoggerFactory.getLogger(QetcherClientImpl.class);
private final HttpClient httpClient;
public QetcherClientImpl(final QetcherClientBuilderImpl builder, final ServiceUriSupplier serviceUriSupplier) {
super(serviceUriSupplier);
this.httpClient = newHttpClient(builder);
}
@VisibleForTesting
protected HttpClient newHttpClient(final QetcherClientBuilderImpl builder) {
final HttpClient.Builder httpClientBuilder = HttpClient.newBuilder();
if (builder.getTrustStore() != null) {
httpClientBuilder.trustStore(builder.getTrustStore());
}
if (builder.getKeyStore() != null) {
httpClientBuilder.keyStore(builder.getKeyStore(), builder.getKeyPassword());
}
return httpClientBuilder
.name("QetcherClient")
.followRedirects(true)
.build();
}
@Override
public void close() {
httpClient.close();
}
@Override
public CompletableFuture uploadFile(final InputConversionFile inputFile) {
final HttpRequest.Builder rb = newRequestBuilder(getFileUploadUri())
.method(getFileUploadMethod(), inputFile.getBodyProvider());
rb.header(FROM_MEDIA_TYPE_HEADER, inputFile.getMediaType().toString());
if (inputFile.getDeleteTimeout() != null) {
rb.header(DELETE_TIMEOUT_HEADER, inputFile.getDeleteTimeout().toString());
}
if (inputFile.getFilename() != null && !inputFile.getFilename().isEmpty()) {
rb.header(FILENAME_HEADER, inputFile.getFilename());
}
return sendForBytes(rb.build())
.thenApply(response -> transformResponse(response, ConversionFile.class));
}
@Override
public CompletableFuture getFile(final String fileId) {
final HttpRequest request = newRequestBuilder(getFileUri(fileId))
.GET()
.build();
return sendForBytes(request)
.thenApply(response -> transformResponse(response, ConversionFile.class));
}
@Override
public CompletableFuture> getFiles() {
final HttpRequest request = newRequestBuilder(getFilesUri())
.GET()
.build();
return sendForBytes(request)
.thenApply(response -> transformResponse(response, ConversionFiles.class))
.thenApply(ConversionFiles::getConversionFiles);
}
@Override
public CompletableFuture deleteFile(final String fileId) {
final HttpRequest request = newRequestBuilder(getFileUri(fileId))
.DELETE(BodyProviders.noBody())
.build();
return sendForBytes(request)
.thenApply(response -> transformResponse(response, Void.class));
}
@Override
public CompletableFuture downloadAsFile(final String fileId, final Path file, final OpenOption... openOptions) {
final HttpRequest request = newRequestBuilder(getFileContentsUri(fileId))
.GET()
.build();
if (LOG.isDebugEnabled()) {
LOG.debug("{} {}", request.method(), request.uri());
}
return httpClient
.sendAsync(request, asSuccessStatusFile(file, openOptions))
.thenApply(HttpResponse::body);
}
private BodyHandler asSuccessStatusFile(final Path file, final OpenOption... openOptions) {
return (status, headers) -> {
requireSuccessStatusCode(status, Optional.empty(), Optional.empty());
return BodyHandlers.asFile(file, openOptions).apply(status, headers);
};
}
@Override
public CompletableFuture downloadAsByteArray(String fileId) {
final HttpRequest request = newRequestBuilder(getFileContentsUri(fileId))
.GET()
.build();
if (LOG.isDebugEnabled()) {
LOG.debug("{} {}", request.method(), request.uri());
}
return httpClient
.sendAsync(request, asSuccessStatusByteArray())
.thenApply(HttpResponse::body);
}
private BodyHandler asSuccessStatusByteArray() {
return (status, headers) -> {
requireSuccessStatusCode(status, Optional.empty(), Optional.empty());
return BodyHandlers.asByteArray().apply(status, headers);
};
}
@Override
public CompletableFuture createJob(final InputJob inputJob) {
if (inputJob.getInputConversionFile() != null) {
return createJobWithUpload(inputJob);
} else {
return createJobForExistingFile(inputJob);
}
}
private CompletableFuture createJobWithUpload(final InputJob inputJob) {
final HttpRequest.Builder rb = newRequestBuilder(getCreateJobForNewFileUri())
.method(getFileUploadMethod(), inputJob.getInputConversionFile().getBodyProvider());
rb.header(FROM_MEDIA_TYPE_HEADER, inputJob.getFromMediaType().toString());
rb.header(TO_MEDIA_TYPE_HEADER, inputJob.getToMediaType().toString());
if (inputJob.getInputConversionFile().getFilename() != null && !inputJob.getInputConversionFile().getFilename().isEmpty()) {
rb.header(FILENAME_HEADER, inputJob.getInputConversionFile().getFilename());
}
if (inputJob.getDeleteTimeout() != null) {
rb.header(DELETE_TIMEOUT_HEADER, inputJob.getDeleteTimeout().toString());
}
if (inputJob.getCancelTimeout() != null) {
rb.header(CANCEL_TIMEOUT_HEADER, inputJob.getCancelTimeout().toString());
}
if (inputJob.getReferrer() != null) {
rb.header(REFERRER_HEADER, inputJob.getReferrer());
}
return sendForBytes(rb.build())
.thenApply(response -> transformResponse(response, Job.class));
}
private CompletableFuture createJobForExistingFile(final InputJob inputJob) {
final HttpRequest.Builder rb = newRequestBuilder(getCreateJobForExistingFileUri())
.method(getCreateJobForExistingFileMethod(), BodyProviders.noBody());
rb.header(CONVERSION_FILE_ID_HEADER, inputJob.getConversionFileIds()
.stream()
.collect(Collectors.joining(",")));
rb.header(TO_MEDIA_TYPE_HEADER, inputJob.getToMediaType().toString());
if (inputJob.getFromMediaType() != null) {
// From media type is optional here. If not given explicitly, the media type
// from the first input file is used
rb.header(FROM_MEDIA_TYPE_HEADER, inputJob.getFromMediaType().toString());
}
if (inputJob.getDeleteTimeout() != null) {
rb.header(DELETE_TIMEOUT_HEADER, inputJob.getDeleteTimeout().toString());
}
if (inputJob.getCancelTimeout() != null) {
rb.header(CANCEL_TIMEOUT_HEADER, inputJob.getCancelTimeout().toString());
}
if (inputJob.getReferrer() != null) {
rb.header(REFERRER_HEADER, inputJob.getReferrer());
}
return sendForBytes(rb.build())
.thenApply(response -> transformResponse(response, Job.class));
}
@Override
public CompletableFuture getJob(final String jobId) {
final HttpRequest request = newRequestBuilder(getJobUri(jobId))
.GET()
.build();
return sendForBytes(request)
.thenApply(response -> transformResponse(response, Job.class));
}
@Override
public CompletableFuture> getJobs() {
final HttpRequest request = newRequestBuilder(getJobsUri())
.GET()
.build();
return sendForBytes(request)
.thenApply(response -> transformResponse(response, Jobs.class))
.thenApply(Jobs::getJobs);
}
@Override
public CompletableFuture deleteJob(final String jobId) {
final HttpRequest request = newRequestBuilder(getJobUri(jobId))
.DELETE(BodyProviders.noBody())
.build();
return sendForBytes(request)
.thenApply(response -> transformResponse(response, Void.class));
}
@Override
public CompletableFuture cancelJob(String jobId) {
final JobPatch jobPatch = Builders.jobPatch()
.cancel()
.build();
final HttpRequest request = newRequestBuilder(getJobUri(jobId))
.method("PATCH", transformRequest(jobPatch))
.build();
return sendForBytes(request)
.thenApply(response -> transformResponse(response, Void.class));
}
@Override
public CompletableFuture> getAvailableConversions() {
final HttpRequest request = newRequestBuilder(getConversionsUri())
.GET()
.build();
return sendForBytes(request)
.thenApply(response -> transformResponse(response, AvailableConversions.class))
.thenApply(AvailableConversions::getAvailableConversions);
}
@Override
public CompletableFuture> getAvailableNodes() {
final URI uri = getAvailableNodesUri();
final HttpRequest request = newRequestBuilder(uri)
.GET()
.build();
return sendForBytes(request)
.thenApply(response -> transformResponse(response, AvailableNodes.class))
.thenApply(AvailableNodes::getAvailableNodes);
}
@Override
public CompletableFuture> getMediaTypes() {
final URI uri = getMediaTypesUri();
final HttpRequest request = newRequestBuilder(uri)
.GET()
.build();
return sendForBytes(request)
.thenApply(response -> transformResponse(response, MediaTypeInfos.class))
.thenApply(MediaTypeInfos::getMediaTypeInfos);
}
@Override
public CompletableFuture getFileExtensions() {
final URI uri = getFileExtensionsUri();
final HttpRequest request = newRequestBuilder(uri)
.GET()
.build();
return sendForBytes(request)
.thenApply(response -> transformResponse(response, FileExtensionInfos.class));
}
@Override
public CompletableFuture getMediaTypeForFilename(final String filename) {
final URI uri = getMediaTypeForFilenameUri(filename);
final HttpRequest request = newRequestBuilder(uri)
.GET()
.build();
return sendForBytes(request)
.thenApply(response -> transformResponse(response, MediaTypeInfo.class));
}
private HttpRequest.Builder newRequestBuilder(final URI uri) {
return HttpRequest.newBuilder()
.uri(uri)
.header(USER_AGENT, USER_AGENT_VALUE);
}
private CompletableFuture> sendForBytes(final HttpRequest request) {
if (LOG.isDebugEnabled()) {
LOG.debug("{} {}", request.method(), request.uri());
}
return httpClient.sendAsync(request, BodyHandlers.asByteArray());
}
private T transformResponse(final HttpResponse response, final Class type) {
return transformResponse(
response.statusCode(),
response.headers().firstValue("Content-Type"),
response.body(),
type);
}
}