All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.mklinger.qetcher.client.impl.AbstractQetcherClient Maven / Gradle / Ivy

There is a newer version: 2.0.42.rc
Show newest version
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.commons.httpclient.BodyProviders;
import de.mklinger.commons.httpclient.HttpRequest.BodyProvider;
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 String MEDIATYPES = "mediatypes";

	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();
	}

	protected URI getMediaTypesUri() {
		return getServiceUriBuilder()
				.pathComponent(V1)
				.pathComponent(MEDIATYPES)
				.build();
	}

	protected URI getFileExtensionsUri() {
		return getServiceUriBuilder()
				.pathComponent(V1)
				.pathComponent(MEDIATYPES)
				.pathComponent("extensions")
				.build();
	}

	protected URI getMediaTypeForFilenameUri(final String filename) {
		return getServiceUriBuilder()
				.pathComponent(V1)
				.pathComponent(MEDIATYPES)
				.pathComponent("filename")
				.pathComponent(filename)
				.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;
	}

	protected BodyProvider transformRequest(Object requestObject) {
		byte[] bytes;
		try {
			bytes = objectMapper.writeValueAsBytes(requestObject);
		} catch (final IOException e) {
			throw new UncheckedIOException(e);
		}

		return BodyProviders.fromByteArray(MediaTypes.JSON.toString(), bytes);
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy