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

de.mklinger.qetcher.liferay.client.impl.QetcherLiferayServiceImpl Maven / Gradle / Ivy

/*
 * Copyright 2013-present mklinger GmbH - http://www.mklinger.de
 *
 * All rights reserved.
 *
 * NOTICE:  All information contained herein is, and remains
 * the property of mklinger GmbH and its suppliers, if any.
 * The intellectual and technical concepts contained herein are
 * proprietary to mklinger GmbH and its suppliers and are protected
 * by trade secret or copyright law. Dissemination of this
 * information or reproduction of this material is strictly forbidden
 * unless prior written permission is obtained from mklinger GmbH.
 */
package de.mklinger.qetcher.liferay.client.impl;

import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Supplier;

import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.liferay.portal.kernel.util.MimeTypesUtil;

import de.mklinger.micro.annotations.VisibleForTesting;
import de.mklinger.micro.keystores.KeyStores;
import de.mklinger.qetcher.client.InputConversionFile;
import de.mklinger.qetcher.client.InputJob;
import de.mklinger.qetcher.client.QetcherClient;
import de.mklinger.qetcher.client.QetcherClientBuilders;
import de.mklinger.qetcher.client.QetcherClientException;
import de.mklinger.qetcher.client.QetcherClientVersion;
import de.mklinger.qetcher.client.httpclient.BodyProviders;
import de.mklinger.qetcher.client.model.v1.AvailableConversion;
import de.mklinger.qetcher.client.model.v1.ConversionFile;
import de.mklinger.qetcher.client.model.v1.FileExtensionInfos;
import de.mklinger.qetcher.client.model.v1.Job;
import de.mklinger.qetcher.client.model.v1.MediaType;
import de.mklinger.qetcher.client.model.v1.MediaTypeInfo;
import de.mklinger.qetcher.client.model.v1.MediaTypes;
import de.mklinger.qetcher.htmlinliner.HtmlElementInliner;
import de.mklinger.qetcher.liferay.client.impl.abstraction.LiferayAbstractionFactorySupplier;

/**
 * @author Marc Klinger - mklinger[at]mklinger[dot]de
 */
public class QetcherLiferayServiceImpl implements QetcherLiferayService {
	private static final String APPLICATION_OCTETSTREAM = "application/octet-stream";

	private static final Logger LOG = LoggerFactory.getLogger(QetcherLiferayServiceImpl.class);

	private volatile QetcherClient qetcherClient;

	private QetcherClient client() {
		if (qetcherClient == null) {
			synchronized (this) {
				if (qetcherClient == null) {
					qetcherClient = newClient();
				}
			}
		}
		return qetcherClient;
	}

	private QetcherClient newClient() {
		final LiferayClientConfiguration configuration = LiferayClientConfiguration.getInstance();

		final KeyStore keyStore = KeyStores.load(
				configuration.getKeyStoreLocation(),
				configuration.getKeyStorePassword().orElse(null),
				configuration.getKeyStoreType().orElse(KeyStore.getDefaultType()));

		final KeyStore trustStore = KeyStores.load(
				configuration.getTrustStoreLocation(),
				configuration.getTrustStorePassword().orElse(null),
				configuration.getTrustStoreType().orElse(KeyStore.getDefaultType()));

		final QetcherClient.Builder clientBuilder = QetcherClientBuilders.client()
				.serviceAddresses(configuration.getServiceAddresses())
				.keyStore(keyStore, configuration.getKeyPassword().orElse(null))
				.trustStore(trustStore);

		final QetcherClient client;

		client = getWithContextClassLoader(getClass().getClassLoader(), clientBuilder::build);

		LOG.info("Initialized new Qetcher client, version {}", QetcherClientVersion.getVersion());
		LOG.info("Using Qetcher service addresses: {}", (Object)configuration.getServiceAddresses());
		new QetcherClientCertificateInfo(keyStore).log();

		return client;
	}

	private  T getWithContextClassLoader(ClassLoader classLoader, Supplier s) {
		final ClassLoader old = Thread.currentThread().getContextClassLoader();
		try {
			LOG.info("Using thread context class loader: {}", classLoader);
			Thread.currentThread().setContextClassLoader(classLoader);
			return s.get();
		} finally {
			Thread.currentThread().setContextClassLoader(old);
		}
	}

	@Override
	public List getAvailableConversions() {
		try {
			return getWithShortTimeout(client().getAvailableConversions());
		} catch (final Exception e) {
			LOG.error("Error getting available conversions from Qetcher service. Returning empty list.", e);
			return Collections.emptyList();
		}
	}

	private static  T getWithShortTimeout(final CompletableFuture future) {
		return get(future, 30, TimeUnit.SECONDS);
	}

	private static  T getWithUploadTimeout(final CompletableFuture future) {
		return get(future, 1, TimeUnit.HOURS);
	}

	private static  T getWithDownloadTimeout(final CompletableFuture future) {
		return get(future, 1, TimeUnit.HOURS);
	}

	private static  T get(final CompletableFuture future, final long timeout, final TimeUnit timeoutUnit) {
		try {
			return future.get(timeout, timeoutUnit);
		} catch (final InterruptedException e) {
			Thread.currentThread().interrupt();
			throw new QetcherClientException("Qetcher client interrupted", e);
		} catch (final TimeoutException e) {
			throw new QetcherClientException("Qetcher client timeout after " + timeout + " " + timeoutUnit, e);
		} catch (final ExecutionException e) {
			throw new QetcherClientException("Qetcher client error", e);
		}
	}

	/**
	 * Convert method for DocumentConversionUtil.
	 */
	@Override
	public File convert(final String id, final InputStream inputStream, final String sourceExtension, final String targetExtension) throws IOException {
		try {
			return doConvert(id, inputStream, sourceExtension, targetExtension, "liferay-conversion-id=" + id);
		} catch (IOException | RuntimeException e) {
			LOG.error("Error converting file", e);
			throw e;
		} catch (final Exception e) {
			LOG.error("Error converting file", e);
			throw new QetcherClientException("Error converting file", e);
		}
	}

	private File doConvert(final String id, final InputStream inputStream, final String sourceExtension, final String targetExtension, final String jobReferer) throws IOException {
		final File targetFile = new File(getFilePath(id, targetExtension));
		final File targetDir = targetFile.getParentFile();
		if (!targetDir.exists()) {
			if (!targetDir.mkdirs()) {
				throw new IOException("Error creating target dir: " + targetDir);
			}
		}
		try (FileOutputStream out = new FileOutputStream(targetFile)) {
			convert(inputStream, out, sourceExtension, targetExtension, null, jobReferer);
		}
		LOG.debug("Saved file to {}", targetFile);
		return targetFile;
	}

	@Override
	public void convert(final InputStream inputStream, final OutputStream outputStream,
			final String sourceExtension, final String targetExtension,
			final Map targetParameters, final String jobReferer) {

		convert(
				inputStream,
				newSingleFileOutputStreamProvider(outputStream),
				sourceExtension,
				targetExtension,
				targetParameters,
				jobReferer);
	}

	@Override
	public void convert(final InputStream inputStream, final OutputStream outputStream,
			final MediaType fromMediaType, final MediaType toMediaType,
			final String jobReferer) throws IOException {

		convert(
				inputStream,
				newSingleFileOutputStreamProvider(outputStream),
				fromMediaType,
				toMediaType,
				jobReferer);
	}

	private OutputStreamProvider newSingleFileOutputStreamProvider(final OutputStream outputStream) {
		return new OutputStreamProvider() {
			private boolean first = true;

			@Override
			public OutputStream getOutputStream(final Job job, final ConversionFile resultFile) {
				if (first) {
					first = false;
					return new NonClosingOutputStream(outputStream);
				}
				return null;
			}
		};
	}

	/**
	 * Convert method for PDFProcessorImpl.
	 */
	@Override
	public void convert(final InputStream inputStream, final OutputStreamProvider outputStreamProvider,
			final String sourceExtension, final String targetExtension, final Map targetParameters,
			final String jobReferer) {

		doConvert(
				inputStream,
				sourceExtension,
				targetExtension,
				targetParameters,
				jobReferer,
				newSingleResultFileCallback(outputStreamProvider));
	}

	private void convert(final InputStream inputStream, final OutputStreamProvider outputStreamProvider,
			final MediaType fromMediaType, final MediaType toMediaType,
			final String jobReferer) {

		doConvert(
				inputStream,
				fromMediaType,
				toMediaType,
				jobReferer,
				newSingleResultFileCallback(outputStreamProvider));
	}

	private ResultFileCallback newSingleResultFileCallback(final OutputStreamProvider outputStreamProvider) {
		return new ResultFileCallback() {
			@Override
			public void processResultFile(final Job job, final ConversionFile resultFile, final QetcherClient client) {
				handleResultFile(job, resultFile, outputStreamProvider, client);
			}
		};
	}

	private void handleResultFile(final Job job, final ConversionFile resultFile, final OutputStreamProvider outputStreamProvider, final QetcherClient client) {
		try (final OutputStream out = outputStreamProvider.getOutputStream(job, resultFile)) {
			if (out != null) {
				try (TmpFile tmpFile = new TmpFile(getWithDownloadTimeout(client.downloadAsTempFile(resultFile.getFileId())))) {
					try (InputStream in = Files.newInputStream(tmpFile.getFile())) {
						IOUtils.copy(in, out);
					}
				}
			} else {
				LOG.info("Ignoring output of job {}, file {}", job.getJobId(), resultFile.getFileId());
			}
		} catch (final IOException e) {
			throw new UncheckedIOException("Error handling result file for job " + job.getJobId(), e);
		}
	}

	private static class TmpFile implements Closeable {
		private final Path file;

		public TmpFile(final Path file) {
			this.file = file;
		}

		public Path getFile() {
			return file;
		}

		@Override
		public void close() throws IOException {
			if (file != null) {
				Files.deleteIfExists(file);
			}
		}
	}

	public interface ResultFileCallback {
		void processResultFile(Job job, ConversionFile resultFile, QetcherClient qetcherClient);
	}

	/** Do the actual conversion. */
	private void doConvert(final InputStream inputStream, final String sourceExtension, final String targetExtension,
			final Map targetParameters, final String jobReferer, final ResultFileCallback callback) {

		LOG.info("Request for conversion from extension '{}' to extension '{}' with referer '{}'", sourceExtension, targetExtension, jobReferer);

		final InputJob inputJob = newInputJob(
				inputStream,
				sourceExtension,
				targetExtension,
				targetParameters,
				jobReferer,
				LiferayClientConfiguration.getInstance());

		runJob(inputJob, jobReferer, callback);
	}

	/** Do the actual conversion. */
	private void doConvert(final InputStream inputStream, final MediaType fromMediaType, final MediaType toMediaType,
			final String jobReferer, final ResultFileCallback callback) {

		LOG.info("Request for conversion from media type '{}' to media type '{}' with referer '{}'", fromMediaType, toMediaType, jobReferer);

		final InputJob inputJob = newInputJob(
				inputStream,
				fromMediaType,
				toMediaType,
				jobReferer,
				LiferayClientConfiguration.getInstance());

		runJob(inputJob, jobReferer, callback);
	}

	private void runJob(final InputJob inputJob, final String jobReferer, final ResultFileCallback callback) {
		final Job createdJob = getWithUploadTimeout(client().createJob(inputJob));

		final String jobId = createdJob.getJobId();

		try {

			final LiferayClientConfiguration configuration = LiferayClientConfiguration.getInstance();
			final long jobWaitTimeoutMillis = configuration.getJobWaitTimeoutMillis();

			final Job doneJob = get(client().getJobDone(createdJob), jobWaitTimeoutMillis, TimeUnit.MILLISECONDS);

			if (doneJob.getResultFileIds().isEmpty()) {
				throw new QetcherClientException("Job did not produce any files. Referer: '" + jobReferer + "'");
			}

			LOG.info("Conversion job '{}' done with referer '{}' from {} to {} ", jobId, inputJob.getReferrer(), inputJob.getFromMediaType(), inputJob.getToMediaType());

			LOG.info("Downloading result files for job '{}' with referer '{}' from {} to {} ", jobId, inputJob.getReferrer(), inputJob.getFromMediaType(), inputJob.getToMediaType());
			// TODO support async result file loading
			doneJob.getResultFileIds().stream()
			.map(resultFileId -> getWithShortTimeout(client().getFile(resultFileId)))
			.forEach(resultFile -> callback.processResultFile(doneJob, resultFile, client()));
			LOG.info("Downloading result files for job '{}' done with referer '{}' from {} to {} ", jobId, inputJob.getReferrer(), inputJob.getFromMediaType(), inputJob.getToMediaType());

		} finally {
			client().deleteJob(jobId).whenComplete((unused, e) -> {
				if (e != null) {
					LOG.warn("Error deleting job '{}'. Referer: '{}'", jobId, jobReferer, e);
				} else {
					LOG.info("Deleted job '{}'. Referer: '{}'", jobId, jobReferer);
				}
			});
		}
	}

	@VisibleForTesting
	protected InputJob newInputJob(final InputStream inputStream, final String sourceExtension,
			final String targetExtension, final Map targetParameters, final String jobReferer,
			final LiferayClientConfiguration configuration) {

		final String sourceMimeTypeString = MimeTypesUtil.getExtensionContentType(sourceExtension);
		if (sourceMimeTypeString == null || APPLICATION_OCTETSTREAM.equals(sourceMimeTypeString)) {
			throw new QetcherClientException("Unable to map source extension to mime type: " + sourceExtension);
		}

		final MediaType fromMediaType = MediaType.valueOf(sourceMimeTypeString);

		String targetMimeTypeString = MimeTypesUtil.getExtensionContentType(targetExtension);
		if (targetMimeTypeString == null || APPLICATION_OCTETSTREAM.equals(targetMimeTypeString)) {
			throw new QetcherClientException("Unable to map target extension to mime type: " + targetExtension);
		}

		if (targetParameters != null && !targetParameters.isEmpty()) {
			targetMimeTypeString = MediaType.valueOf(targetMimeTypeString).withParameters(targetParameters).toString();
		}
		final MediaType toMediaType = MediaType.valueOf(targetMimeTypeString);

		return newInputJob(inputStream, fromMediaType, toMediaType, jobReferer, configuration);
	}

	private InputJob newInputJob(final InputStream inputStream, final MediaType fromMediaType,
			final MediaType toMediaType, final String jobReferer, final LiferayClientConfiguration configuration) {

		LOG.info("Conversion with referer '{}' from {} to {} ", jobReferer, fromMediaType, toMediaType);

		final InputStream actualInputStream = getAugmentedInputStream(fromMediaType, inputStream);

		final InputConversionFile inputConversionFile = QetcherClientBuilders.inputFile()
				.mediaType(fromMediaType)
				.bodyProvider(BodyProviders.fromInputStream(() -> actualInputStream))
				.build();

		return QetcherClientBuilders.job()
				.fromFile(inputConversionFile)
				.toMediaType(toMediaType)
				.referrer(jobReferer)
				.deleteTimeout(Duration.ofMillis(configuration.getJobDeleteTimeoutMillis()))
				.cancelTimeout(Duration.ofMillis(configuration.getJobCancelTimeoutMillis()))
				.build();
	}

	private InputStream getAugmentedInputStream(final MediaType fromMediaType, final InputStream inputStream) {
		if (MediaTypes.HTML.isCompatible(fromMediaType)) {
			final String baseUrl = LiferayAbstractionFactorySupplier.getInstance().getPortalTool().getBaseUrl();
			if (baseUrl == null) {
				LOG.warn("Could not get base url - no html inlining can be done");
			} else {
				try (HtmlElementInliner inliner = LiferayHtmlElementInlinerFactory.newHtmlElementInliner()) {
					final byte[] inlinedHtml = inliner.inline(inputStream, baseUrl);
					return new ByteArrayInputStream(inlinedHtml);
				} catch (final Exception e) {
					LOG.warn("Error inlining html elements", e);
				}
			}
		}
		return inputStream;
	}

	@Override
	// TODO this method also exists in DocumentConversionUtil
	public String getFilePath(final String id, final String targetExtension) {
		final StringBuilder sb = new StringBuilder(5);
		sb.append(LiferayClientConfiguration.getInstance().getDocumentConversionTargetPath());
		sb.append('/');
		sb.append(id);
		sb.append('.');
		sb.append(targetExtension);
		return sb.toString();
	}

	@Override
	public FileExtensionInfos getFileExtensions() {
		try {
			return getWithShortTimeout(client().getFileExtensions());
		} catch (final Exception e) {
			LOG.warn("Error getting file extension infos from Qetcher service. Returning empty object.", e);
			return new FileExtensionInfos(Collections.emptyMap());
		}
	}

	@Override
	public Optional getMediaTypeForFilename(final String filename) {
		try {
			return Optional.of(getWithShortTimeout(client().getMediaTypeForFilename(filename)));
		} catch (final Exception e) {
			LOG.info("Error getting media type for filename from Qetcher service. Returning empty optional.");
			return Optional.empty();
		}
	}

	@Override
	public List getMediaTypes() {
		try {
			return getWithShortTimeout(client().getMediaTypes());
		} catch (final Exception e) {
			LOG.warn("Error getting media types from Qetcher service. Returning empty list.", e);
			return Collections.emptyList();
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy