Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
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();
}
}
}