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

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

There is a newer version: 2.0.42.rc
Show newest version
/*
 * 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.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

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

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.Job;
import de.mklinger.qetcher.client.model.v1.MediaType;
import de.mklinger.qetcher.client.model.v1.MediaTypes;
import de.mklinger.qetcher.htmlinliner.HtmlElementInliner;
import de.mklinger.qetcher.liferay.abstraction.LiferayException;
import de.mklinger.qetcher.liferay.client.impl.abstraction.LiferayAbstractionFactorySupplier;
import de.mklinger.qetcher.liferay.client.impl.mediatype.MediaTypesService;

/**
 * @author Marc Klinger - mklinger[at]mklinger[dot]de
 */
public class QetcherLiferayServiceImpl implements QetcherLiferayService {
	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 client = QetcherClientBuilders.client()
				.serviceAddresses(configuration.getServiceAddresses())
				.keyStore(keyStore, configuration.getKeyPassword().orElse(null))
				.trustStore(trustStore)
				.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;
	}

	@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.");
			return Collections.emptyList();
		}
	}

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

	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, LiferayException {
		try {
			return doConvert(id, inputStream, sourceExtension, targetExtension, "liferay-conversion-id=" + id);
		} catch (IOException | LiferayException | 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, LiferayException {
		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) throws IOException, LiferayException {
		convert(inputStream, 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;
			}
		}, sourceExtension, targetExtension, targetParameters, jobReferer);
	}

	/**
	 * 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) throws IOException, LiferayException {
		doConvert(inputStream, sourceExtension, targetExtension, targetParameters, jobReferer, 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 LiferayClientConfiguration configuration = LiferayClientConfiguration.getInstance();

		final InputJob inputJob = newInputJob(
				inputStream,
				sourceExtension,
				targetExtension,
				targetParameters,
				jobReferer,
				configuration);

		final Job createdJob = getWithShortTimeout(client().createJob(inputJob));

		final String jobId = createdJob.getJobId();

		try {

			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 = MediaTypesService.getInstance().getMimeTypeStringByExtension(sourceExtension)
				.orElseThrow(() -> new QetcherClientException("Unable to map source extension to mime type: " + sourceExtension));

		final MediaType fromMediaType = MediaType.valueOf(sourceMimeTypeString);

		String targetMimeTypeString = MediaTypesService.getInstance().getMimeTypeStringByExtension(targetExtension)
				.orElseThrow(() -> 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);

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

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




© 2015 - 2024 Weber Informatics LLC | Privacy Policy