de.mklinger.qetcher.client.impl.AbstractQetcherClient Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of qetcher-client-bundle Show documentation
Show all versions of qetcher-client-bundle Show documentation
Qetcher Java client, OSGi bundle, minimal dependencies
package de.mklinger.qetcher.client.impl;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.mklinger.micro.uribuilder.UriBuilder;
import de.mklinger.qetcher.client.QetcherClient;
import de.mklinger.qetcher.client.QetcherClientException;
import de.mklinger.qetcher.client.QetcherRemoteException;
import de.mklinger.qetcher.client.common.concurrent.Delay;
import de.mklinger.qetcher.client.impl.lookup.ServiceUriSupplier;
import de.mklinger.qetcher.client.model.v1.ConversionFile;
import de.mklinger.qetcher.client.model.v1.Error;
import de.mklinger.qetcher.client.model.v1.Job;
import de.mklinger.qetcher.client.model.v1.MediaType;
import de.mklinger.qetcher.client.model.v1.MediaTypes;
import de.mklinger.qetcher.client.model.v1.builder.ErrorBuilder;
import de.mklinger.qetcher.client.model.v1.jackson.ObjectMapperConfigurer;
/**
* Qetcher client base class.
*
* This is the base class for all HTTP Qetcher clients. Is has no knowledge about
* the underlying HTTP client implementation.
*
* It provides default implementations for some {@link QetcherClient} methods and
* protected helper methods for subclasses, dealing with HTTP methods and
* service URIs.
*
* @author Marc Klinger - mklinger[at]mklinger[dot]de
*/
public abstract class AbstractQetcherClient implements QetcherClient {
private static final String POST = "POST";
private static final String V1 = "v1";
private static final String FILES = "files";
private static final String JOBS = "jobs";
private static final String CONVERSIONS = "conversions";
private static final String NODES = "nodes";
private static final ObjectMapper objectMapper = ObjectMapperConfigurer.configure(new ObjectMapper());
private static final Logger LOG = LoggerFactory.getLogger(AbstractQetcherClient.class);
private final ServiceUriSupplier serviceUriSupplier;
public AbstractQetcherClient(final ServiceUriSupplier serviceUriSupplier) {
this.serviceUriSupplier = serviceUriSupplier;
}
public ServiceUriSupplier getServiceUriSupplier() {
return serviceUriSupplier;
}
@Override
public CompletableFuture getFile(final ConversionFile file) {
return getFile(file.getFileId());
}
@Override
public CompletableFuture deleteFile(final ConversionFile file) {
return deleteFile(file.getFileId());
}
@Override
public CompletableFuture downloadAsFile(final String fileId, final Path file) {
return downloadAsFile(fileId, file, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
}
@Override
public CompletableFuture downloadAsFile(final String fileId, final File file) {
return downloadAsFile(fileId, file.toPath())
.thenApply(Path::toFile);
}
@Override
public CompletableFuture downloadAsTempFile(final String fileId) {
Path file;
try {
file = Files.createTempFile(fileId, ".tmp");
} catch (final IOException e) {
throw new UncheckedIOException(e);
}
return downloadAsFile(fileId, file);
}
@Override
public CompletableFuture downloadAsTempFile(final String fileId, final Path dir) {
Path file;
try {
file = Files.createTempFile(dir, fileId, ".tmp");
} catch (final IOException e) {
throw new UncheckedIOException(e);
}
return downloadAsFile(fileId, file);
}
@Override
public CompletableFuture getJob(final Job job) {
return getJob(job.getJobId());
}
@Override
public CompletableFuture deleteJob(final Job job) {
return deleteJob(job.getJobId());
}
@Override
public CompletableFuture getJobDone(final Job job) {
return getJobDone(job.getJobId());
}
@Override
public CompletableFuture getJobDone(final String jobId) {
final CompletableFuture cf = new CompletableFuture<>();
new JobStatePollerUntilDone(jobId, cf).run();
return cf;
}
private class JobStatePollerUntilDone implements Runnable {
private final String jobId;
private final CompletableFuture cf;
private final AtomicLong nextDelayMillis;
public JobStatePollerUntilDone(final String jobId, final CompletableFuture cf) {
this.jobId = jobId;
this.cf = cf;
this.nextDelayMillis = new AtomicLong(100);
}
@Override
public void run() {
getJob(jobId).thenAccept(job -> {
switch(job.getState()) {
case INITIALIZING:
case IN_PROGRESS:
case PAUSED:
case WAITING:
final long delayMillis = nextDelayMillis.getAndUpdate(this::getNextDelay);
Delay.delayedExecutor(delayMillis, TimeUnit.MILLISECONDS).execute(this);
break;
case CANCELED:
case ERROR:
case SUCCESS:
cf.complete(job);
break;
default:
throw new IllegalStateException("Unsupported job state: " + job.getState());
}
})
.exceptionally(e -> {
cf.completeExceptionally(e);
return null;
});
}
private long getNextDelay(final long currentDelay) {
if (currentDelay >= 5000) {
return 5000;
}
final long nextDelay = (long)(1.5 * currentDelay);
if (nextDelay >= 5000) {
return 5000;
} else {
return nextDelay;
}
}
}
protected URI getFileUploadUri() {
return getFilesUri();
}
protected String getFileUploadMethod() {
return POST;
}
protected URI getFileUri(final String fileId) {
return getServiceUriBuilder()
.pathComponent(V1)
.pathComponent(FILES)
.pathComponent(fileId)
.build();
}
protected URI getFileContentsUri(final String fileId) {
return getServiceUriBuilder()
.pathComponent(V1)
.pathComponent(FILES)
.pathComponent(fileId)
.pathComponent("contents")
.build();
}
protected URI getFilesUri() {
return getServiceUriBuilder()
.pathComponent(V1)
.pathComponent(FILES)
.build();
}
protected URI getCreateJobForExistingFileUri() {
return getJobsUri();
}
protected String getCreateJobForExistingFileMethod() {
return POST;
}
protected URI getCreateJobForNewFileUri() {
return getJobsUri();
}
protected String getCreateJobForNewFileMethod() {
return POST;
}
protected URI getJobUri(final String jobId) {
return getServiceUriBuilder()
.pathComponent(V1)
.pathComponent(JOBS)
.pathComponent(jobId)
.build();
}
protected URI getJobsUri() {
return getServiceUriBuilder()
.pathComponent(V1)
.pathComponent(JOBS)
.build();
}
protected URI getConversionsUri() {
return getServiceUriBuilder()
.pathComponent(V1)
.pathComponent(CONVERSIONS)
.build();
}
protected URI getAvailableNodesUri() {
return getServiceUriBuilder()
.pathComponent(V1)
.pathComponent(NODES)
.build();
}
private UriBuilder getServiceUriBuilder() {
return UriBuilder.of(getServiceUri());
}
private URI getServiceUri() {
return serviceUriSupplier.get();
}
protected T transformResponse(final int statusCode, final Optional contentType, final byte[] responseBody, final Class type) {
requireSuccessStatusCode(statusCode, contentType, Optional.of(responseBody));
return getResponseObject(contentType, responseBody, type);
}
protected void requireSuccessStatusCode(final int statusCode, final Optional contentType, final Optional responseBody) {
LOG.debug("Status {}", statusCode);
if (!isSuccessStatusCode(statusCode)) {
final Error error = getErrorObject(statusCode, contentType, responseBody);
throw new QetcherRemoteException(error, statusCode);
}
}
private Error getErrorObject(final int statusCode, final Optional contentType, final Optional responseBody) {
if (responseBody.isPresent()) {
try {
return getResponseObject(contentType, responseBody.get(), Error.class);
} catch (final Exception e) {
// ignore
LOG.info("Unable to get error response object", e);
}
}
return new ErrorBuilder()
.status(String.valueOf(statusCode))
.message("No remote error information available")
.build();
}
private T getResponseObject(final Optional contentType, final byte[] responseBody, final Class type) {
if (contentType.isPresent() && !MediaTypes.JSON.isCompatible(MediaType.valueOf(contentType.get()))) {
// TODO also support smile and use it by default
throw new QetcherClientException("Unsupported content type: " + contentType.get());
}
if (type == Void.class) {
return null;
}
try {
return objectMapper.readValue(responseBody, type);
} catch (final IOException e) {
throw new UncheckedIOException(e);
}
}
private boolean isSuccessStatusCode(final int statusCode) {
return statusCode >= 200 && statusCode < 300;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy