+
+
+~~~
+
+* *TransformerName*Controller.java - A [Spring Boot](https://projects.spring.io/spring-boot/) Controller that
+ extends AbstractTransformerController to handel requests. It implements a few methods including *transformImpl*
+ which is intended to perform the actual transform. Generally the transform is done in a sub class of
+ *JavaExecutor*, when a Java library is being used or *AbstractCommandExecutor*, when an external process is used.
+ Both are sub interfaces of *Transformer*.
+
+~~~
+...
+@Controller
+public class TransformerNameController extends AbstractTransformerController
+{
+ private static final Logger logger = LoggerFactory.getLogger(TransformerNameController.class);
+
+ TransformerNameExecutor executor;
+
+ @PostConstruct
+ private void init()
+ {
+ executor = new TransformerNameExecutor();
+ }
+
+ @Override
+ public String getTransformerName()
+ {
+ return "Transformer Name";
+ }
+
+ @Override
+ public String version()
+ {
+ return commandExecutor.version();
+ }
+
+ @Override
+ public ProbeTestTransform getProbeTestTransform()
+ {
+ // See the Javadoc on this method and Probes.md for the choice of these values.
+ return new ProbeTestTransform(this, "quick.pdf", "quick.png",
+ 7455, 1024, 150, 10240, 60 * 20 + 1, 60 * 15 - 15)
+ {
+ @Override
+ protected void executeTransformCommand(File sourceFile, File targetFile)
+ {
+ transformImpl(null, null, null, Collections.emptyMap(), sourceFile, targetFile);
+ }
+ };
+ }
+
+ @Override
+ public void transformImpl(String transformName, String sourceMimetype, String targetMimetype,
+ Map transformOptions, File sourceFile, File targetFile)
+ {
+ executor.transform(sourceMimetype, targetMimetype, transformOptions, sourceFile, targetFile);
+ }
+}
+~~~
+
+* *TransformerName*Executer.java - *JavaExecutor* and *CommandExecutor* sub classes need to extract values from
+ *transformOptions* and use them in a call to an external process or as parameters to a library call.
+~~~
+...
+public class TransformerNameExecutor extends AbstractCommandExecutor
+{
+ ...
+ @Override
+ public void transform(String transformName, String sourceMimetype, String targetMimetype,
+ Map transformOptions,
+ File sourceFile, File targetFile) throws TransformException
+ {
+ final String options = TransformerNameOptionsBuilder
+ .builder()
+ .withWidth(transformOptions.get(WIDTH_REQUEST_PARAM))
+ .withHeight(transformOptions.get(HEIGHT_REQUEST_PARAM))
+ .build();
+
+ Long timeout = stringToLong(transformOptions.get(TIMEOUT));
+
+ run(options, sourceFile, targetFile, timeout);
+ }
+}
+~~~
+
+* Application.java - [Spring Boot](https://projects.spring.io/spring-boot/) expects to find an Application in
+ a project's source files. The following may be used:
+
+~~~
+package org.alfresco.transformer;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application
+{
+ public static void main(String[] args)
+ {
+ SpringApplication.run(Application.class, args);
+ }
+}
+~~~
+
+Transform requests are handled by the *AbstractTransformerController*, but are either:
+ * POST requests (a direct http request from a client) where the transform options are passed as parameters, the source is supplied as a multipart file and
+ the response is a file download.
+ * POST request (a request via a message queue) where the transform options are supplied as JSON and the response is also JSON.
+ The source and target content is read from a location accessible to both the client and the transfomer.
+
+**Example JSON request body**
+```javascript
+var transformRequest = {
+ "requestId": "1",
+ "sourceReference": "2f9ed237-c734-4366-8c8b-6001819169a4",
+ "sourceMediaType": "application/pdf",
+ "sourceSize": 123456,
+ "sourceExtension": "pdf",
+ "targetMediaType": "text/plain",
+ "targetExtension": "txt",
+ "clientType": "ACS",
+ "clientData": "Yo No Soy Marinero, Soy Capitan, Soy Capitan!",
+ "schema": 1,
+ "transformRequestOptions": {
+ "targetMimetype": "text/plain",
+ "targetEncoding": "UTF-8",
+ "abc:width": "120",
+ "abc:height": "200"
+ }
+}
+```
+
+**Example JSON response body**
+
+```javascript
+var transformReply = {
+ "requestId": "1",
+ "status": 201,
+ "errorDetails": null,
+ "sourceReference": "2f9ed237-c734-4366-8c8b-6001819169a4",
+ "targetReference": "34d69ff0-7eaa-4741-8a9f-e1915e6995bf",
+ "clientType": "ACS",
+ "clientData": "Yo No Soy Marinero, Soy Capitan, Soy Capitan!",
+ "schema": 1
+}
+```
+
+## Building and testing
+
+The project can be built by running the Maven command:
+
+~~~
+mvn clean install
+~~~
+
+## Artifacts
+
+The artifacts can be obtained by:
+
+* downloading from the [Alfresco repository](https://artifacts.alfresco.com/nexus/content/groups/public/)
+* Adding a Maven dependency to your pom file.
+
+~~~
+
+ org.alfresco
+ alfresco-t-engine-base
+ 1.0
+
+~~~
+
+and the Alfresco Maven repository:
+
+~~~
+
+ alfresco-maven-repo
+ https://artifacts.alfresco.com/nexus/content/groups/public
+
+~~~
+
+The build plan is available in [TravisCI](https://travis-ci.com/Alfresco/alfresco-transform-core).
+
+## Contributing guide
+
+Please use [this guide](https://github.com/Alfresco/alfresco-repository/blob/master/CONTRIBUTING.md)
+to make a contribution to the project.
+
diff --git a/t-engine-base/pom.xml b/t-engine-base/pom.xml
new file mode 100644
index 00000000..95fdb002
--- /dev/null
+++ b/t-engine-base/pom.xml
@@ -0,0 +1,101 @@
+
+
+ 4.0.0
+
+
+ org.alfresco
+ alfresco-transform-core
+ 2.6.1-SNAPSHOT
+
+
+ alfresco-t-engine-base
+
+
+ false
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ io.micrometer
+ micrometer-registry-prometheus
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ com.vaadin.external.google
+ android-json
+
+
+
+
+
+ org.dom4j
+ dom4j
+
+
+
+ org.alfresco
+ alfresco-transform-model
+ ${project.version}
+
+
+ org.springframework.boot
+ spring-boot-starter-activemq
+
+
+ org.apache.activemq
+ activemq-client
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+
+
+ org.messaginghub
+ pooled-jms
+
+
+ com.google.collections
+ google-collections
+ 1.0
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ test-jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-deploy-plugin
+
+ ${transformer.base.deploy.skip}
+
+
+
+
+
diff --git a/alfresco-transform-tika/alfresco-transform-tika-boot/src/main/java/org/alfresco/transformer/Application.java b/t-engine-base/src/main/java/org/alfresco/transform/base/Application.java
similarity index 79%
rename from alfresco-transform-tika/alfresco-transform-tika-boot/src/main/java/org/alfresco/transformer/Application.java
rename to t-engine-base/src/main/java/org/alfresco/transform/base/Application.java
index d95be427..582da7d4 100644
--- a/alfresco-transform-tika/alfresco-transform-tika-boot/src/main/java/org/alfresco/transformer/Application.java
+++ b/t-engine-base/src/main/java/org/alfresco/transform/base/Application.java
@@ -2,7 +2,7 @@
* #%L
* Alfresco Transform Core
* %%
- * Copyright (C) 2005 - 2020 Alfresco Software Limited
+ * Copyright (C) 2005 - 2022 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* -
@@ -24,12 +24,12 @@
* along with Alfresco. If not, see .
* #L%
*/
-package org.alfresco.transformer;
+package org.alfresco.transform.base;
import io.micrometer.core.instrument.MeterRegistry;
-import org.alfresco.transformer.executors.TikaJavaExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
@@ -41,15 +41,19 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.event.EventListener;
import java.util.Arrays;
+import java.util.List;
-import static org.alfresco.transformer.logging.StandardMessages.LICENCE;
+import static org.alfresco.transform.base.logging.StandardMessages.LICENCE;
@SpringBootApplication
-@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
+@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class})
public class Application
{
private static final Logger logger = LoggerFactory.getLogger(Application.class);
+ @Autowired(required = false)
+ private List transformEngines;
+
@Value("${container.name}")
private String containerName;
@@ -69,7 +73,11 @@ public class Application
{
logger.info("--------------------------------------------------------------------------------------------------------------------------------------------------------------");
Arrays.stream(LICENCE.split("\\n")).forEach(logger::info);
- Arrays.stream(TikaJavaExecutor.LICENCE.split("\\n")).forEach(logger::info);
+ if (transformEngines != null) {
+ transformEngines.stream()
+ .map(transformEngine -> transformEngine.getStartupMessage())
+ .forEach(message -> Arrays.stream(message.split("\\n")).forEach(logger::info));
+ }
logger.info("--------------------------------------------------------------------------------------------------------------------------------------------------------------");
logger.info("Starting application components... Done");
diff --git a/t-engine-base/src/main/java/org/alfresco/transform/base/CustomTransformer.java b/t-engine-base/src/main/java/org/alfresco/transform/base/CustomTransformer.java
new file mode 100644
index 00000000..f36948cd
--- /dev/null
+++ b/t-engine-base/src/main/java/org/alfresco/transform/base/CustomTransformer.java
@@ -0,0 +1,40 @@
+/*
+ * #%L
+ * Alfresco Transform Core
+ * %%
+ * Copyright (C) 2005 - 2022 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.transform.base;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Map;
+
+public interface CustomTransformer
+{
+ String getTransformerName();
+
+ void transform(String sourceMimetype, String sourceEncoding, InputStream inputStream,
+ String targetMimetype, String targetEncoding, OutputStream outputStream,
+ Map transformOptions) throws Exception;
+}
diff --git a/t-engine-base/src/main/java/org/alfresco/transform/base/QueueTransformService.java b/t-engine-base/src/main/java/org/alfresco/transform/base/QueueTransformService.java
new file mode 100644
index 00000000..fb500916
--- /dev/null
+++ b/t-engine-base/src/main/java/org/alfresco/transform/base/QueueTransformService.java
@@ -0,0 +1,208 @@
+/*
+ * #%L
+ * Alfresco Transform Core
+ * %%
+ * Copyright (C) 2005 - 2022 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.transform.base;
+
+import static org.springframework.http.HttpStatus.BAD_REQUEST;
+import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
+
+import java.util.Optional;
+
+import javax.jms.Destination;
+import javax.jms.JMSException;
+import javax.jms.Message;
+
+import org.alfresco.transform.client.model.TransformReply;
+import org.alfresco.transform.client.model.TransformRequest;
+import org.alfresco.transform.common.TransformException;
+import org.alfresco.transform.base.messaging.TransformMessageConverter;
+import org.alfresco.transform.base.messaging.TransformReplySender;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.http.HttpStatus;
+import org.springframework.jms.annotation.JmsListener;
+import org.springframework.jms.support.converter.MessageConversionException;
+import org.springframework.stereotype.Component;
+
+/**
+ * Queue Transformer service.
+ * This service reads all the requests for the particular engine, forwards them to the worker
+ * component (at this time the injected controller - to be refactored) and sends back the reply
+ * to the {@link Message#getJMSReplyTo()} value. If this value is missing we've got to a dead end.
+ *
+ * @author Lucian Tuca
+ * created on 18/12/2018
+ */
+@Component
+@ConditionalOnProperty(name = "activemq.url")
+public class QueueTransformService
+{
+ private static final Logger logger = LoggerFactory.getLogger(QueueTransformService.class);
+
+ // TODO: I know this is not smart but all the the transformation logic is in the Controller.
+ // The controller also manages the probes. There's tons of refactoring needed there, hence this. Sorry.
+ @Autowired
+ private TransformController transformController;
+
+ @Autowired
+ private TransformMessageConverter transformMessageConverter;
+
+ @Autowired
+ private TransformReplySender transformReplySender;
+
+ @JmsListener(destination = "${queue.engineRequestQueue}", concurrency = "${jms-listener.concurrency}")
+ public void receive(final Message msg)
+ {
+ if (msg == null)
+ {
+ logger.error("Received null message!");
+ return;
+ }
+
+ final String correlationId = tryRetrieveCorrelationId(msg);
+ Destination replyToDestinationQueue;
+
+ try
+ {
+ replyToDestinationQueue = msg.getJMSReplyTo();
+ if (replyToDestinationQueue == null)
+ {
+ logger.error(
+ "Cannot find 'replyTo' destination queue for message with correlationID {}. Stopping. ",
+ correlationId);
+ return;
+ }
+ }
+ catch (JMSException e)
+ {
+ logger.error(
+ "Cannot find 'replyTo' destination queue for message with correlationID {}. Stopping. ",
+ correlationId);
+ return;
+ }
+
+ logger.trace("New T-Request from queue with correlationId: {}", correlationId);
+
+ Optional transformRequest;
+ try
+ {
+ transformRequest = convert(msg, correlationId);
+ }
+ catch (TransformException e)
+ {
+ logger.error(e.getMessage(), e);
+ replyWithError(replyToDestinationQueue, HttpStatus.valueOf(e.getStatusCode()),
+ e.getMessage(), correlationId);
+ return;
+ }
+
+ if (!transformRequest.isPresent())
+ {
+ logger.error("T-Request from message with correlationID {} is null!", correlationId);
+ replyWithInternalSvErr(replyToDestinationQueue,
+ "JMS exception during T-Request deserialization: ", correlationId);
+ return;
+ }
+
+ TransformReply reply = transformController.transform(transformRequest.get(), null)
+ .getBody();
+
+ transformReplySender.send(replyToDestinationQueue, reply);
+ }
+
+ /**
+ * Tries to convert the JMS {@link Message} to a {@link TransformRequest}
+ * If any error occurs, a {@link TransformException} is thrown
+ *
+ * @param msg Message to be deserialized
+ * @return The converted {@link TransformRequest} instance
+ */
+ private Optional convert(final Message msg, String correlationId)
+ {
+ try
+ {
+ TransformRequest request = (TransformRequest) transformMessageConverter
+ .fromMessage(msg);
+ return Optional.ofNullable(request);
+ }
+ catch (MessageConversionException e)
+ {
+ String message =
+ "MessageConversionException during T-Request deserialization of message with correlationID "
+ + correlationId + ": ";
+ throw new TransformException(BAD_REQUEST.value(), message + e.getMessage());
+ }
+ catch (JMSException e)
+ {
+ String message =
+ "JMSException during T-Request deserialization of message with correlationID "
+ + correlationId + ": ";
+ throw new TransformException(INTERNAL_SERVER_ERROR.value(),
+ message + e.getMessage());
+ }
+ catch (Exception e)
+ {
+ String message =
+ "Exception during T-Request deserialization of message with correlationID "
+ + correlationId + ": ";
+ throw new TransformException(INTERNAL_SERVER_ERROR.value(),
+ message + e.getMessage());
+ }
+ }
+
+ private void replyWithInternalSvErr(final Destination destination, final String msg,
+ final String correlationId)
+ {
+ replyWithError(destination, INTERNAL_SERVER_ERROR, msg, correlationId);
+ }
+
+ private void replyWithError(final Destination destination, final HttpStatus status,
+ final String msg,
+ final String correlationId)
+ {
+ final TransformReply reply = TransformReply
+ .builder()
+ .withStatus(status.value())
+ .withErrorDetails(msg)
+ .build();
+
+ transformReplySender.send(destination, reply, correlationId);
+ }
+
+ private static String tryRetrieveCorrelationId(final Message msg)
+ {
+ try
+ {
+ return msg.getJMSCorrelationID();
+ }
+ catch (Exception ignore)
+ {
+ return null;
+ }
+ }
+}
diff --git a/t-engine-base/src/main/java/org/alfresco/transform/base/TransformController.java b/t-engine-base/src/main/java/org/alfresco/transform/base/TransformController.java
new file mode 100644
index 00000000..2c5a9a54
--- /dev/null
+++ b/t-engine-base/src/main/java/org/alfresco/transform/base/TransformController.java
@@ -0,0 +1,662 @@
+/*
+ * #%L
+ * Alfresco Transform Core
+ * %%
+ * Copyright (C) 2005 - 2022 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.transform.base;
+
+import org.alfresco.transform.base.probes.ProbeTestTransform;
+import org.alfresco.transform.common.TransformerDebug;
+import org.alfresco.transform.client.model.InternalContext;
+import org.alfresco.transform.client.model.TransformReply;
+import org.alfresco.transform.client.model.TransformRequest;
+import org.alfresco.transform.messages.TransformRequestValidator;
+import org.alfresco.transform.config.TransformConfig;
+import org.alfresco.transform.registry.TransformServiceRegistry;
+import org.alfresco.transform.common.TransformException;
+import org.alfresco.transform.base.clients.AlfrescoSharedFileStoreClient;
+import org.alfresco.transform.base.logging.LogEntry;
+import org.alfresco.transform.base.model.FileRefResponse;
+import org.codehaus.plexus.util.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.TypeMismatchException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.Resource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.validation.DirectFieldBindingResult;
+import org.springframework.validation.Errors;
+import org.springframework.web.bind.MissingServletRequestParameterException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.PostConstruct;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static java.text.MessageFormat.format;
+import static java.util.stream.Collectors.joining;
+import static org.alfresco.transform.config.CoreVersionDecorator.setOrClearCoreVersion;
+import static org.alfresco.transform.common.RequestParamMap.DIRECT_ACCESS_URL;
+import static org.alfresco.transform.common.RequestParamMap.CONFIG_VERSION;
+import static org.alfresco.transform.common.RequestParamMap.CONFIG_VERSION_DEFAULT;
+import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_TRANSFORM;
+import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_TRANSFORM_CONFIG;
+import static org.alfresco.transform.base.fs.FileManager.TempFileProvider.createTempFile;
+import static org.alfresco.transform.base.fs.FileManager.buildFile;
+import static org.alfresco.transform.base.fs.FileManager.createAttachment;
+import static org.alfresco.transform.base.fs.FileManager.createSourceFile;
+import static org.alfresco.transform.base.fs.FileManager.createTargetFile;
+import static org.alfresco.transform.base.fs.FileManager.createTargetFileName;
+import static org.alfresco.transform.base.fs.FileManager.deleteFile;
+import static org.alfresco.transform.base.fs.FileManager.getFilenameFromContentDisposition;
+import static org.alfresco.transform.base.fs.FileManager.save;
+import static org.alfresco.transform.base.util.RequestParamMap.FILE;
+import static org.alfresco.transform.base.util.RequestParamMap.SOURCE_ENCODING;
+import static org.alfresco.transform.base.util.RequestParamMap.SOURCE_EXTENSION;
+import static org.alfresco.transform.base.util.RequestParamMap.SOURCE_MIMETYPE;
+import static org.alfresco.transform.base.util.RequestParamMap.TARGET_MIMETYPE;
+import static org.springframework.http.HttpStatus.BAD_REQUEST;
+import static org.springframework.http.HttpStatus.CREATED;
+import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
+import static org.springframework.http.HttpStatus.OK;
+import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
+import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;
+import static org.springframework.util.StringUtils.getFilenameExtension;
+
+/**
+ * Provides the main endpoints into the t-engine.
+ */
+@Controller
+public class TransformController
+{
+ private static final Logger logger = LoggerFactory.getLogger(TransformController.class);
+ private static final List NON_TRANSFORM_OPTION_REQUEST_PARAMETERS = Arrays.asList(SOURCE_EXTENSION,
+ TARGET_MIMETYPE, SOURCE_MIMETYPE, DIRECT_ACCESS_URL);
+
+ @Autowired(required = false)
+ private List transformEngines;
+
+ @Autowired(required = false)
+ private List customTransformers;
+
+ @Autowired
+ private AlfrescoSharedFileStoreClient alfrescoSharedFileStoreClient;
+ @Autowired
+ private TransformRequestValidator transformRequestValidator;
+ @Autowired
+ private TransformServiceRegistry transformRegistry;
+ @Autowired
+ private TransformerDebug transformerDebug;
+ @Value("${transform.core.version}")
+ private String coreVersion;
+
+ private TransformEngine transformEngine;
+ ProbeTestTransform probeTestTransform;
+ private Map customTransformersByName = new HashMap<>();
+ private AtomicInteger httpRequestCount = new AtomicInteger(1);
+
+ @PostConstruct
+ public void init()
+ {
+ initTransformEngine();
+ initProbeTestTransform();
+ initCustomTransformersByName();
+ }
+
+ private void initTransformEngine()
+ {
+ if (transformEngines != null)
+ {
+ // Normally there is just one TransformEngine per t-engine, but we also want to be able to amalgamate the
+ // CustomTransform code from many t-engines into a single t-engine. In this case, there should be a wrapper
+ // TransformEngine (it has no TransformConfig of its own).
+ transformEngine = transformEngines.stream()
+ .filter(transformEngine -> transformEngine.getTransformConfig() == null)
+ .findFirst()
+ .orElse(transformEngines.get(0));
+ }
+ }
+
+ private void initProbeTestTransform()
+ {
+ if (transformEngine != null)
+ {
+ probeTestTransform = transformEngine.getLivenessAndReadinessProbeTestTransform();
+ }
+ }
+
+ private void initCustomTransformersByName()
+ {
+ if (customTransformers != null)
+ {
+ customTransformers.forEach(customTransformer -> customTransformersByName.put(customTransformer.getTransformerName(),
+ customTransformer));
+ }
+ }
+
+ /**
+ * @return a string that may be used in client debug.
+ */
+ @RequestMapping("/version")
+ @ResponseBody
+ public String version()
+ {
+ return transformEngine.getTransformEngineName() + ' ' + coreVersion + " available";
+ }
+
+ /**
+ * Test UI page to perform a transform.
+ */
+ @GetMapping("/")
+ public String transformForm(Model model)
+ {
+ return "transformForm";
+ }
+
+ /**
+ * Test UI error page.
+ */
+ @GetMapping("/error")
+ public String error()
+ {
+ return "error"; // the name of the template
+ }
+
+ /**
+ * Test UI log page.
+ */
+ @GetMapping("/log")
+ String log(Model model)
+ {
+ model.addAttribute("title", transformEngine.getTransformEngineName() + " Log Entries");
+ Collection log = LogEntry.getLog();
+ if (!log.isEmpty())
+ {
+ model.addAttribute("log", log);
+ }
+ return "log"; // the name of the template
+ }
+
+ /**
+ * Kubernetes readiness probe.
+ */
+ @GetMapping("/ready")
+ @ResponseBody
+ public String ready(HttpServletRequest request)
+ {
+ return probeTestTransform.doTransformOrNothing(request, false, this);
+ }
+
+ /**
+ * Kubernetes liveness probe.
+ */
+ @GetMapping("/live")
+ @ResponseBody
+ public String live(HttpServletRequest request)
+ {
+ return probeTestTransform.doTransformOrNothing(request, true, this);
+ }
+
+ @GetMapping(value = ENDPOINT_TRANSFORM_CONFIG)
+ public ResponseEntity transformConfig(
+ @RequestParam(value = CONFIG_VERSION, defaultValue = CONFIG_VERSION_DEFAULT) int configVersion)
+ {
+ logger.info("GET Transform Config version: " + configVersion);
+ TransformConfig transformConfig = ((TransformRegistryImpl) transformRegistry).getTransformConfig();
+ transformConfig = setOrClearCoreVersion(transformConfig, configVersion);
+ return new ResponseEntity<>(transformConfig, OK);
+ }
+
+ @PostMapping(value = ENDPOINT_TRANSFORM, consumes = MULTIPART_FORM_DATA_VALUE)
+ public ResponseEntity transform(HttpServletRequest request,
+ @RequestParam(value = FILE, required = false) MultipartFile sourceMultipartFile,
+ @RequestParam(value = SOURCE_MIMETYPE, required = false) String sourceMimetype,
+ @RequestParam(value = TARGET_MIMETYPE, required = false) String targetMimetype,
+ @RequestParam Map requestParameters)
+ {
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("Processing request via HTTP endpoint. Params: sourceMimetype: '{}', targetMimetype: '{}', "
+ + "requestParameters: {}", sourceMimetype, targetMimetype, requestParameters);
+ }
+
+ final String directUrl = requestParameters.getOrDefault(DIRECT_ACCESS_URL, "");
+
+ File sourceFile;
+ String sourceFilename;
+ if (directUrl.isBlank())
+ {
+ if (sourceMultipartFile == null)
+ {
+ throw new TransformException(BAD_REQUEST.value(), "Required request part 'file' is not present");
+ }
+ sourceFile = createSourceFile(request, sourceMultipartFile);
+ sourceFilename = sourceMultipartFile.getOriginalFilename();
+ }
+ else
+ {
+ sourceFile = getSourceFileFromDirectUrl(directUrl);
+ sourceFilename = sourceFile.getName();
+ }
+
+ final String targetFilename = createTargetFileName(sourceFilename, sourceMimetype, targetMimetype);
+ probeTestTransform.incrementTransformerCount();
+ final File targetFile = createTargetFile(request, targetFilename);
+
+ Map transformOptions = getTransformOptions(requestParameters);
+ String transformName = getTransformerName(sourceFile, sourceMimetype, targetMimetype, transformOptions);
+ String reference = "e"+httpRequestCount.getAndIncrement();
+ transformerDebug.pushTransform(reference, sourceMimetype, targetMimetype, sourceFile, transformName);
+ transformerDebug.logOptions(reference, requestParameters);
+ try
+ {
+ transformImpl(transformName, sourceMimetype, targetMimetype, transformOptions, sourceFile, targetFile);
+
+ final ResponseEntity body = createAttachment(targetFilename, targetFile);
+ LogEntry.setTargetSize(targetFile.length());
+ long time = LogEntry.setStatusCodeAndMessage(OK.value(), "Success");
+ probeTestTransform.recordTransformTime(time);
+ transformerDebug.popTransform(reference, time);
+ return body;
+ }
+ catch (Throwable t)
+ {
+ transformerDebug.logFailure(reference, t.getMessage());
+ throw t;
+ }
+ }
+
+ /**
+ * '/transform' endpoint which consumes and produces 'application/json'
+ *
+ * This is the way to tell Spring to redirect the request to this endpoint
+ * instead of the one which produces 'html'
+ *
+ * @param request The transformation request
+ * @param timeout Transformation timeout
+ * @return A transformation reply
+ */
+ @PostMapping(value = ENDPOINT_TRANSFORM, produces = APPLICATION_JSON_VALUE)
+ @ResponseBody
+ public ResponseEntity transform(@RequestBody TransformRequest request,
+ @RequestParam(value = "timeout", required = false) Long timeout)
+ {
+ logger.trace("Received {}, timeout {} ms", request, timeout);
+
+ final TransformReply reply = new TransformReply();
+ reply.setRequestId(request.getRequestId());
+ reply.setSourceReference(request.getSourceReference());
+ reply.setSchema(request.getSchema());
+ reply.setClientData(request.getClientData());
+
+ final Errors errors = validateTransformRequest(request);
+ validateInternalContext(request, errors);
+ initialiseContext(request);
+ reply.setInternalContext(request.getInternalContext());
+ if (!errors.getAllErrors().isEmpty())
+ {
+ reply.setStatus(BAD_REQUEST.value());
+ reply.setErrorDetails(errors
+ .getAllErrors()
+ .stream()
+ .map(Object::toString)
+ .collect(joining(", ")));
+
+ transformerDebug.logFailure(reply);
+ logger.trace("Invalid request, sending {}", reply);
+ return new ResponseEntity<>(reply, HttpStatus.valueOf(reply.getStatus()));
+ }
+ transformerDebug.pushTransform(request);
+
+ // Load the source file
+ File sourceFile;
+ try
+ {
+ final String directUrl = request.getTransformRequestOptions().getOrDefault(DIRECT_ACCESS_URL, "");
+ if (directUrl.isBlank())
+ {
+ sourceFile = loadSourceFile(request.getSourceReference(), request.getSourceExtension());
+ }
+ else
+ {
+ sourceFile = getSourceFileFromDirectUrl(directUrl);
+ }
+ }
+ catch (TransformException e)
+ {
+ reply.setStatus(e.getStatusCode());
+ reply.setErrorDetails(messageWithCause("Failed at reading the source file", e));
+
+ transformerDebug.logFailure(reply);
+ logger.trace("Failed to load source file (TransformException), sending " + reply);
+ return new ResponseEntity<>(reply, HttpStatus.valueOf(reply.getStatus()));
+ }
+ catch (HttpClientErrorException e)
+ {
+ reply.setStatus(e.getStatusCode().value());
+ reply.setErrorDetails(messageWithCause("Failed at reading the source file", e));
+
+ transformerDebug.logFailure(reply);
+ logger.trace("Failed to load source file (HttpClientErrorException), sending " + reply, e);
+ return new ResponseEntity<>(reply, HttpStatus.valueOf(reply.getStatus()));
+ }
+ catch (Exception e)
+ {
+ reply.setStatus(INTERNAL_SERVER_ERROR.value());
+ reply.setErrorDetails(messageWithCause("Failed at reading the source file", e));
+
+ transformerDebug.logFailure(reply);
+ logger.trace("Failed to load source file (Exception), sending " + reply, e);
+ return new ResponseEntity<>(reply, HttpStatus.valueOf(reply.getStatus()));
+ }
+
+ // Create local temp target file in order to run the transformation
+ final String targetFilename = createTargetFileName(sourceFile.getName(), request.getTargetMediaType(), request.getSourceMediaType());
+ final File targetFile = buildFile(targetFilename);
+
+ // Run the transformation
+ try
+ {
+ String targetMimetype = request.getTargetMediaType();
+ String sourceMimetype = request.getSourceMediaType();
+ Map transformOptions = getTransformOptions(request.getTransformRequestOptions());
+ transformerDebug.logOptions(request);
+ String transformName = getTransformerName(sourceFile, sourceMimetype, targetMimetype, transformOptions);
+ transformImpl(transformName, sourceMimetype, targetMimetype, transformOptions, sourceFile, targetFile);
+ reply.getInternalContext().setCurrentSourceSize(targetFile.length());
+ }
+ catch (TransformException e)
+ {
+ reply.setStatus(e.getStatusCode());
+ reply.setErrorDetails(messageWithCause("Failed at processing transformation", e));
+
+ transformerDebug.logFailure(reply);
+ logger.trace("Failed to perform transform (TransformException), sending " + reply, e);
+ return new ResponseEntity<>(reply, HttpStatus.valueOf(reply.getStatus()));
+ }
+ catch (Exception e)
+ {
+ reply.setStatus(INTERNAL_SERVER_ERROR.value());
+ reply.setErrorDetails(messageWithCause("Failed at processing transformation", e));
+
+ transformerDebug.logFailure(reply);
+ logger.trace("Failed to perform transform (Exception), sending " + reply, e);
+ return new ResponseEntity<>(reply, HttpStatus.valueOf(reply.getStatus()));
+ }
+
+ // Write the target file
+ FileRefResponse targetRef;
+ try
+ {
+ targetRef = alfrescoSharedFileStoreClient.saveFile(targetFile);
+ }
+ catch (TransformException e)
+ {
+ reply.setStatus(e.getStatusCode());
+ reply.setErrorDetails(messageWithCause("Failed at writing the transformed file", e));
+
+ transformerDebug.logFailure(reply);
+ logger.trace("Failed to save target file (TransformException), sending " + reply, e);
+ return new ResponseEntity<>(reply, HttpStatus.valueOf(reply.getStatus()));
+ }
+ catch (HttpClientErrorException e)
+ {
+ reply.setStatus(e.getStatusCode().value());
+ reply.setErrorDetails(messageWithCause("Failed at writing the transformed file. ", e));
+
+ transformerDebug.logFailure(reply);
+ logger.trace("Failed to save target file (HttpClientErrorException), sending " + reply, e);
+ return new ResponseEntity<>(reply, HttpStatus.valueOf(reply.getStatus()));
+ }
+ catch (Exception e)
+ {
+ reply.setStatus(INTERNAL_SERVER_ERROR.value());
+ reply.setErrorDetails(messageWithCause("Failed at writing the transformed file. ", e));
+
+ transformerDebug.logFailure(reply);
+ logger.trace("Failed to save target file (Exception), sending " + reply, e);
+ return new ResponseEntity<>(reply, HttpStatus.valueOf(reply.getStatus()));
+ }
+
+ try
+ {
+ deleteFile(targetFile);
+ }
+ catch (Exception e)
+ {
+ logger.error("Failed to delete local temp target file '{}'. Error will be ignored ",
+ targetFile, e);
+ }
+ try
+ {
+ deleteFile(sourceFile);
+ }
+ catch (Exception e)
+ {
+ logger.error("Failed to delete source local temp file " + sourceFile, e);
+ }
+
+ reply.setTargetReference(targetRef.getEntry().getFileRef());
+ reply.setStatus(CREATED.value());
+
+ transformerDebug.popTransform(reply);
+ logger.trace("Sending successful {}, timeout {} ms", reply, timeout);
+ return new ResponseEntity<>(reply, HttpStatus.valueOf(reply.getStatus()));
+ }
+
+ private Errors validateTransformRequest(final TransformRequest transformRequest)
+ {
+ DirectFieldBindingResult errors = new DirectFieldBindingResult(transformRequest, "request");
+ transformRequestValidator.validate(transformRequest, errors);
+ return errors;
+ }
+
+ private void validateInternalContext(TransformRequest request, Errors errors)
+ {
+ String errorMessage = InternalContext.checkForBasicErrors(request.getInternalContext(), "T-Request");
+ if (errorMessage != null)
+ {
+ errors.rejectValue("internalContext", null, errorMessage);
+ }
+ }
+
+ private void initialiseContext(TransformRequest request)
+ {
+ // If needed, initialise the context enough to allow logging to take place without NPE checks
+ request.setInternalContext(InternalContext.initialise(request.getInternalContext()));
+ }
+
+ private File getSourceFileFromDirectUrl(String directUrl)
+ {
+ File sourceFile = createTempFile("tmp", ".tmp");
+ try
+ {
+ FileUtils.copyURLToFile(new URL(directUrl), sourceFile);
+ }
+ catch (IllegalArgumentException e)
+ {
+ throw new TransformException(BAD_REQUEST.value(), "Direct Access Url is invalid.", e);
+ }
+ catch (IOException e)
+ {
+ throw new TransformException(BAD_REQUEST.value(), "Direct Access Url not found.", e);
+ }
+
+ return sourceFile;
+ }
+
+ protected Map getTransformOptions(Map requestParameters)
+ {
+ Map transformOptions = new HashMap<>(requestParameters);
+ transformOptions.keySet().removeAll(NON_TRANSFORM_OPTION_REQUEST_PARAMETERS);
+ transformOptions.values().removeIf(v -> v.isEmpty());
+ return transformOptions;
+ }
+
+ /**
+ * Loads the file with the specified sourceReference from Alfresco Shared File Store
+ *
+ * @param sourceReference reference to the file in Alfresco Shared File Store
+ * @param sourceExtension default extension if the file in Alfresco Shared File Store has none
+ * @return the file containing the source content for the transformation
+ */
+ private File loadSourceFile(final String sourceReference, final String sourceExtension)
+ {
+ ResponseEntity responseEntity = alfrescoSharedFileStoreClient
+ .retrieveFile(sourceReference);
+ probeTestTransform.incrementTransformerCount();
+
+ HttpHeaders headers = responseEntity.getHeaders();
+ String filename = getFilenameFromContentDisposition(headers);
+
+ String extension = getFilenameExtension(filename) != null ? getFilenameExtension(filename) : sourceExtension;
+ MediaType contentType = headers.getContentType();
+ long size = headers.getContentLength();
+
+ final Resource body = responseEntity.getBody();
+ if (body == null)
+ {
+ String message = "Source file with reference: " + sourceReference + " is null or empty. "
+ + "Transformation will fail and stop now as there is no content to be transformed.";
+ logger.warn(message);
+ throw new TransformException(BAD_REQUEST.value(), message);
+ }
+ final File file = createTempFile("source_", "." + extension);
+
+ logger.debug("Read source content {} length={} contentType={}",
+ sourceReference, size, contentType);
+
+ save(body, file);
+ LogEntry.setSource(filename, size);
+ return file;
+ }
+
+ private static String messageWithCause(final String prefix, Throwable e)
+ {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(prefix).append(" - ")
+ .append(e.getClass().getSimpleName()).append(": ")
+ .append(e.getMessage());
+
+ while (e.getCause() != null)
+ {
+ e = e.getCause();
+ sb.append(", cause ")
+ .append(e.getClass().getSimpleName()).append(": ")
+ .append(e.getMessage());
+ }
+
+ return sb.toString();
+ }
+
+ private String getTransformerName(final File sourceFile, final String sourceMimetype,
+ final String targetMimetype, final Map transformOptions)
+ {
+ // The transformOptions always contains sourceEncoding when sent to a T-Engine, even though it should not be
+ // used to select a transformer. Similar to source and target mimetypes and extensions, but these are not
+ // passed in transformOptions.
+ String sourceEncoding = transformOptions.remove(SOURCE_ENCODING);
+ try
+ {
+ final long sourceSizeInBytes = sourceFile.length();
+ final String transformerName = transformRegistry.findTransformerName(sourceMimetype,
+ sourceSizeInBytes, targetMimetype, transformOptions, null);
+ if (transformerName == null)
+ {
+ throw new TransformException(BAD_REQUEST.value(), "No transforms were able to handle the request");
+ }
+ return transformerName;
+ }
+ finally
+ {
+ if (sourceEncoding != null)
+ {
+ transformOptions.put(SOURCE_ENCODING, sourceEncoding);
+ }
+ }
+ }
+
+ public void transformImpl(String transformName, String sourceMimetype, String targetMimetype,
+ Map transformOptions, File sourceFile, File targetFile)
+ {
+ //javaExecutor.transformExtractOrEmbed(transformName, sourceMimetype, targetMimetype, transformOptions, sourceFile, targetFile);
+ }
+
+ @ExceptionHandler(TypeMismatchException.class)
+ public void handleParamsTypeMismatch(HttpServletResponse response, MissingServletRequestParameterException e) throws IOException
+ {
+ final String message = format("Request parameter ''{0}'' is of the wrong type", e.getParameterName());
+ final int statusCode = BAD_REQUEST.value();
+
+ logger.error(message, e);
+ LogEntry.setStatusCodeAndMessage(statusCode, message);
+ response.sendError(statusCode, transformEngine.getTransformEngineName() + " - " + message);
+ }
+
+ @ExceptionHandler(MissingServletRequestParameterException.class)
+ public void handleMissingParams(HttpServletResponse response, MissingServletRequestParameterException e) throws IOException
+ {
+ final String message = format("Request parameter ''{0}'' is missing", e.getParameterName());
+ final int statusCode = BAD_REQUEST.value();
+
+ logger.error(message, e);
+ LogEntry.setStatusCodeAndMessage(statusCode, message);
+ response.sendError(statusCode, transformEngine.getTransformEngineName() + " - " + message);
+ }
+
+ @ExceptionHandler(TransformException.class)
+ public void transformExceptionWithMessage(HttpServletResponse response, TransformException e) throws IOException
+ {
+ final String message = e.getMessage();
+ final int statusCode = e.getStatusCode();
+
+ logger.error(message, e);
+ long time = LogEntry.setStatusCodeAndMessage(statusCode, message);
+ probeTestTransform.recordTransformTime(time);
+ response.sendError(statusCode, transformEngine.getTransformEngineName() + " - " + message);
+ }
+}
diff --git a/t-engine-base/src/main/java/org/alfresco/transform/base/TransformEngine.java b/t-engine-base/src/main/java/org/alfresco/transform/base/TransformEngine.java
new file mode 100644
index 00000000..527cb449
--- /dev/null
+++ b/t-engine-base/src/main/java/org/alfresco/transform/base/TransformEngine.java
@@ -0,0 +1,56 @@
+/*
+ * #%L
+ * Alfresco Transform Core
+ * %%
+ * Copyright (C) 2005 - 2022 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.transform.base;
+
+import org.alfresco.transform.config.TransformConfig;
+import org.alfresco.transform.base.probes.ProbeTestTransform;
+
+/**
+ * The interface to the custom transform code applied on top of a base t-engine.
+ */
+public interface TransformEngine
+{
+ /**
+ * @return the name of the t-engine. The t-router reads config from t-engines in name order.
+ */
+ String getTransformEngineName();
+
+ /**
+ * @return messages to be logged on start up (license & settings). Use \n to split onto multiple lines.
+ */
+ String getStartupMessage();
+
+ /**
+ * @return a definition of what the t-engine supports. Normally read from a json Resource on the classpath.
+ */
+ TransformConfig getTransformConfig();
+
+ /**
+ * @return a ProbeTestTransform (will do a quick transform) for k8 liveness and readiness probes.
+ */
+ ProbeTestTransform getLivenessAndReadinessProbeTestTransform();
+}
diff --git a/t-engine-base/src/main/java/org/alfresco/transform/base/TransformInterceptor.java b/t-engine-base/src/main/java/org/alfresco/transform/base/TransformInterceptor.java
new file mode 100644
index 00000000..16f5d3f2
--- /dev/null
+++ b/t-engine-base/src/main/java/org/alfresco/transform/base/TransformInterceptor.java
@@ -0,0 +1,64 @@
+/*
+ * #%L
+ * Alfresco Transform Core
+ * %%
+ * Copyright (C) 2005 - 2022 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.transform.base;
+
+import static org.alfresco.transform.base.fs.FileManager.SOURCE_FILE;
+import static org.alfresco.transform.base.fs.FileManager.TARGET_FILE;
+import static org.alfresco.transform.base.fs.FileManager.deleteFile;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.alfresco.transform.base.logging.LogEntry;
+import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
+
+/**
+ * TransformInterceptor
+ *
+ * Handles ThreadLocal Log entries for each request.
+ */
+public class TransformInterceptor extends HandlerInterceptorAdapter
+{
+ @Override
+ public boolean preHandle(HttpServletRequest request,
+ HttpServletResponse response, Object handler)
+ {
+ LogEntry.start();
+ return true;
+ }
+
+ @Override
+ public void afterCompletion(HttpServletRequest request,
+ HttpServletResponse response, Object handler, Exception ex)
+ {
+ // TargetFile cannot be deleted until completion, otherwise 0 bytes are sent.
+ deleteFile(request, SOURCE_FILE);
+ deleteFile(request, TARGET_FILE);
+
+ LogEntry.complete();
+ }
+}
diff --git a/t-engine-base/src/main/java/org/alfresco/transform/base/TransformRegistryImpl.java b/t-engine-base/src/main/java/org/alfresco/transform/base/TransformRegistryImpl.java
new file mode 100644
index 00000000..c4ae7812
--- /dev/null
+++ b/t-engine-base/src/main/java/org/alfresco/transform/base/TransformRegistryImpl.java
@@ -0,0 +1,106 @@
+/*
+ * #%L
+ * Alfresco Repository
+ * %%
+ * Copyright (C) 2005 - 2022 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.transform.base;
+
+import org.alfresco.transform.config.TransformConfig;
+import org.alfresco.transform.registry.AbstractTransformRegistry;
+import org.alfresco.transform.registry.CombinedTransformConfig;
+import org.alfresco.transform.registry.TransformCache;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+
+import javax.annotation.PostConstruct;
+import java.util.Comparator;
+import java.util.List;
+
+import static org.alfresco.transform.config.CoreVersionDecorator.setCoreVersionOnSingleStepTransformers;
+
+/**
+ * Used by clients to work out if a transformation is supported based on the engine_config.json.
+ */
+public class TransformRegistryImpl extends AbstractTransformRegistry
+{
+ private static final Logger log = LoggerFactory.getLogger(TransformRegistryImpl.class);
+
+ @Autowired(required = false)
+ private List transformEngines;
+
+ @Value("${transform.core.version}")
+ private String coreVersion;
+
+ private TransformConfig transformConfigBeforeIncompleteTransformsAreRemoved;
+
+ @PostConstruct
+ public void init()
+ {
+ CombinedTransformConfig combinedTransformConfig = new CombinedTransformConfig();
+ if (transformEngines != null)
+ {
+ transformEngines.stream()
+ .sorted(Comparator.comparing(TransformEngine::getTransformEngineName))
+ .forEach(transformEngine -> {
+ TransformConfig transformConfig = transformEngine.getTransformConfig();
+ if (transformConfig != null)
+ {
+ setCoreVersionOnSingleStepTransformers(transformConfig, coreVersion);
+ combinedTransformConfig.addTransformConfig(transformConfig,
+ transformEngine.getTransformEngineName(), "---", this);
+ }
+ });
+ }
+ transformConfigBeforeIncompleteTransformsAreRemoved = combinedTransformConfig.buildTransformConfig();
+ combinedTransformConfig.combineTransformerConfig(this);
+ combinedTransformConfig.registerCombinedTransformers(this);
+ }
+
+ // Unlike other subclasses this class does not extend Data or replace it at run time.
+ private TransformCache data = new TransformCache();
+
+ public TransformConfig getTransformConfig()
+ {
+ return transformConfigBeforeIncompleteTransformsAreRemoved;
+ }
+
+ @Override
+ public TransformCache getData()
+ {
+ return data;
+ }
+
+ @Override
+ protected void logError(String msg)
+ {
+ log.error(msg);
+ }
+
+ @Override
+ protected void logWarn(String msg)
+ {
+ log.warn(msg);
+ }
+}
diff --git a/t-engine-base/src/main/java/org/alfresco/transform/base/clients/AlfrescoSharedFileStoreClient.java b/t-engine-base/src/main/java/org/alfresco/transform/base/clients/AlfrescoSharedFileStoreClient.java
new file mode 100644
index 00000000..bd608712
--- /dev/null
+++ b/t-engine-base/src/main/java/org/alfresco/transform/base/clients/AlfrescoSharedFileStoreClient.java
@@ -0,0 +1,103 @@
+/*
+ * #%L
+ * Alfresco Transform Core
+ * %%
+ * Copyright (C) 2005 - 2022 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.transform.base.clients;
+
+import static org.springframework.http.HttpMethod.POST;
+import static org.springframework.http.MediaType.MULTIPART_FORM_DATA;
+
+import java.io.File;
+
+import org.alfresco.transform.common.TransformException;
+import org.alfresco.transform.base.model.FileRefResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.core.io.Resource;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * Simple Rest client that call Alfresco Shared File Store
+ */
+public class AlfrescoSharedFileStoreClient
+{
+ @Value("${fileStoreUrl}")
+ private String fileStoreUrl;
+
+ @Autowired
+ private RestTemplate restTemplate;
+
+ /**
+ * Retrieves a file from Shared File Store using given file reference
+ *
+ * @param fileRef File reference
+ * @return ResponseEntity
+ */
+ public ResponseEntity retrieveFile(String fileRef)
+ {
+ try
+ {
+ return restTemplate.getForEntity(fileStoreUrl + "/" + fileRef,
+ org.springframework.core.io.Resource.class);
+ }
+ catch (HttpClientErrorException e)
+ {
+ throw new TransformException(e.getStatusCode().value(), e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Stores given file in Shared File Store
+ *
+ * @param file File to be stored
+ * @return A FileRefResponse containing detail about file's reference
+ */
+ public FileRefResponse saveFile(File file)
+ {
+ try
+ {
+ FileSystemResource value = new FileSystemResource(file.getAbsolutePath());
+ LinkedMultiValueMap map = new LinkedMultiValueMap<>();
+ map.add("file", value);
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MULTIPART_FORM_DATA);
+ HttpEntity> requestEntity = new HttpEntity<>(map,
+ headers);
+ ResponseEntity responseEntity = restTemplate
+ .exchange(fileStoreUrl, POST, requestEntity, FileRefResponse.class);
+ return responseEntity.getBody();
+ }
+ catch (HttpClientErrorException e)
+ {
+ throw new TransformException(e.getStatusCode().value(), e.getMessage(), e);
+ }
+ }
+}
diff --git a/t-engine-base/src/main/java/org/alfresco/transform/base/config/WebApplicationConfig.java b/t-engine-base/src/main/java/org/alfresco/transform/base/config/WebApplicationConfig.java
new file mode 100644
index 00000000..b122d692
--- /dev/null
+++ b/t-engine-base/src/main/java/org/alfresco/transform/base/config/WebApplicationConfig.java
@@ -0,0 +1,95 @@
+/*
+ * #%L
+ * Alfresco Transform Core
+ * %%
+ * Copyright (C) 2005 - 2022 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.transform.base.config;
+
+import org.alfresco.transform.base.TransformInterceptor;
+import org.alfresco.transform.base.TransformRegistryImpl;
+import org.alfresco.transform.base.clients.AlfrescoSharedFileStoreClient;
+import org.alfresco.transform.common.TransformerDebug;
+import org.alfresco.transform.messages.TransformRequestValidator;
+import org.alfresco.transform.registry.TransformServiceRegistry;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_TRANSFORM;
+
+@Configuration
+@ComponentScan(basePackages = {"org.alfresco.transform"})
+public class WebApplicationConfig implements WebMvcConfigurer
+{
+ @Override
+ public void addInterceptors(InterceptorRegistry registry)
+ {
+ registry
+ .addInterceptor(transformInterceptor())
+ .addPathPatterns(ENDPOINT_TRANSFORM, "/live", "/ready");
+ }
+
+ @Bean
+ public TransformInterceptor transformInterceptor()
+ {
+ return new TransformInterceptor();
+ }
+
+ @Bean
+ public RestTemplate restTemplate()
+ {
+ return new RestTemplate();
+ }
+
+ @Bean
+ public AlfrescoSharedFileStoreClient alfrescoSharedFileStoreClient()
+ {
+ return new AlfrescoSharedFileStoreClient();
+ }
+
+ @Bean
+ public TransformRequestValidator transformRequestValidator()
+ {
+ return new TransformRequestValidator();
+ }
+
+ @Autowired Environment env;
+
+ @Bean
+ public TransformServiceRegistry transformRegistry()
+ {
+ return new TransformRegistryImpl();
+ }
+
+ @Bean
+ public TransformerDebug transformerDebug()
+ {
+ return new TransformerDebug().setIsTEngine(true);
+ }
+}
diff --git a/t-engine-base/src/main/java/org/alfresco/transform/base/executors/AbstractCommandExecutor.java b/t-engine-base/src/main/java/org/alfresco/transform/base/executors/AbstractCommandExecutor.java
new file mode 100644
index 00000000..f48f886a
--- /dev/null
+++ b/t-engine-base/src/main/java/org/alfresco/transform/base/executors/AbstractCommandExecutor.java
@@ -0,0 +1,91 @@
+/*
+ * #%L
+ * Alfresco Transform Core
+ * %%
+ * Copyright (C) 2005 - 2022 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.transform.base.executors;
+
+import static org.alfresco.transform.base.executors.RuntimeExec.ExecutionResult;
+import static org.springframework.http.HttpStatus.BAD_REQUEST;
+import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
+
+import java.io.File;
+import java.util.Map;
+
+import org.alfresco.transform.common.TransformException;
+
+/**
+ *
+ */
+public abstract class AbstractCommandExecutor implements CommandExecutor
+{
+ protected RuntimeExec transformCommand = createTransformCommand();
+ protected RuntimeExec checkCommand = createCheckCommand();
+
+ protected abstract RuntimeExec createTransformCommand();
+
+ protected abstract RuntimeExec createCheckCommand();
+
+ @Override
+ public void run(Map properties, File targetFile, Long timeout)
+ {
+ timeout = timeout != null && timeout > 0 ? timeout : 0;
+ final ExecutionResult result = transformCommand.execute(properties, timeout);
+
+ if (result.getExitValue() != 0 && result.getStdErr() != null && result.getStdErr().length() > 0)
+ {
+ throw new TransformException(BAD_REQUEST.value(),
+ "Transformer exit code was not 0: \n" + result.getStdErr());
+ }
+
+ if (!targetFile.exists() || targetFile.length() == 0)
+ {
+ throw new TransformException(INTERNAL_SERVER_ERROR.value(),
+ "Transformer failed to create an output file");
+ }
+ }
+
+ @Override
+ public String version()
+ {
+ if (checkCommand != null)
+ {
+ final ExecutionResult result = checkCommand.execute();
+ if (result.getExitValue() != 0 && result.getStdErr() != null && result.getStdErr().length() > 0)
+ {
+ throw new TransformException(INTERNAL_SERVER_ERROR.value(),
+ "Transformer version check exit code was not 0: \n" + result);
+ }
+
+ final String version = result.getStdOut().trim();
+ if (version.isEmpty())
+ {
+ throw new TransformException(INTERNAL_SERVER_ERROR.value(),
+ "Transformer version check failed to create any output");
+ }
+ return version;
+ }
+ return "Version not checked";
+ }
+}
diff --git a/t-engine-base/src/main/java/org/alfresco/transform/base/executors/CommandExecutor.java b/t-engine-base/src/main/java/org/alfresco/transform/base/executors/CommandExecutor.java
new file mode 100644
index 00000000..45cf3041
--- /dev/null
+++ b/t-engine-base/src/main/java/org/alfresco/transform/base/executors/CommandExecutor.java
@@ -0,0 +1,71 @@
+/*
+ * #%L
+ * Alfresco Transform Core
+ * %%
+ * Copyright (C) 2005 - 2022 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.transform.base.executors;
+
+import org.alfresco.transform.base.logging.LogEntry;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Basic interface for executing transformations via Shell commands
+ *
+ * @author Cezar Leahu
+ */
+public interface CommandExecutor extends Transformer
+{
+ void run(Map properties, File targetFile, Long timeout);
+
+ String version();
+
+ default void run(String options, File sourceFile, File targetFile,
+ Long timeout)
+ {
+ LogEntry.setOptions(options);
+
+ Map properties = new HashMap<>();
+ properties.put("options", options);
+ properties.put("source", sourceFile.getAbsolutePath());
+ properties.put("target", targetFile.getAbsolutePath());
+
+ run(properties, targetFile, timeout);
+ }
+
+ default void run(String options, File sourceFile, String pageRange, File targetFile,
+ Long timeout)
+ {
+ LogEntry.setOptions(pageRange + (pageRange.isEmpty() ? "" : " ") + options);
+
+ Map properties = new HashMap<>();
+ properties.put("options", options);
+ properties.put("source", sourceFile.getAbsolutePath() + pageRange);
+ properties.put("target", targetFile.getAbsolutePath());
+
+ run(properties, targetFile, timeout);
+ }
+}
diff --git a/t-engine-base/src/main/java/org/alfresco/transform/base/executors/ExecParameterTokenizer.java b/t-engine-base/src/main/java/org/alfresco/transform/base/executors/ExecParameterTokenizer.java
new file mode 100644
index 00000000..313fe597
--- /dev/null
+++ b/t-engine-base/src/main/java/org/alfresco/transform/base/executors/ExecParameterTokenizer.java
@@ -0,0 +1,360 @@
+/*
+ * #%L
+ * Alfresco Transform Core
+ * %%
+ * Copyright (C) 2005 - 2022 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.transform.base.executors;
+
+import static java.util.Collections.singletonList;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.StringTokenizer;
+
+/**
+ * DUPLICATED FROM *alfresco-core*.
+ *
+ * This class is used to tokenize strings used as parameters for {@link RuntimeExec} objects.
+ * Examples of such strings are as follows (ImageMagick-like parameters):
+ *
-font Helvetica -pointsize 50 -draw "gravity south fill black text 0,12 'CopyRight'"
+ *
+ * The first is the simple case which would be parsed into Strings as follows:
+ * "-font", "Helvetica", "-pointsize", "50"
+ *
+ * The second is more complex in that it includes a quoted parameter, which would be parsed as a single String:
+ * "-font", "Helvetica", "-pointsize", "50", "circle 100,100 150,150"
+ * Note however that the quotation characters will be stripped from the token.
+ *
+ * The third shows an example with embedded quotation marks, which would parse to:
+ * "-font", "Helvetica", "-pointsize", "50", "gravity south fill black text 0,12 'CopyRight'"
+ * In this case, the embedded quotation marks (which must be different from those surrounding the parameter)
+ * are preserved in the extracted token.
+ *
+ * The class does not understand escaped quotes such as p1 p2 "a b c \"hello\" d" p4
+ *
+ * @author Neil Mc Erlean
+ * @since 3.4.2
+ */
+public class ExecParameterTokenizer
+{
+ /**
+ * The string to be tokenized.
+ */
+ private final String str;
+
+ /**
+ * The list of tokens, which will take account of quoted sections.
+ */
+ private List tokens;
+
+ public ExecParameterTokenizer(String str)
+ {
+ this.str = str;
+ }
+
+ /**
+ * This method returns the tokens in a parameter string.
+ * Any tokens not contained within single or double quotes will be tokenized in the normal
+ * way i.e. by using whitespace separators and the standard StringTokenizer algorithm.
+ * Any tokens which are contained within single or double quotes will be returned as single
+ * String instances and will have their quote marks removed.
+ *
+ * See above for examples.
+ *
+ * @throws NullPointerException if the string to be tokenized was null.
+ */
+ public List getAllTokens()
+ {
+ if (this.str == null)
+ {
+ throw new NullPointerException("Illegal null string cannot be tokenized.");
+ }
+
+ if (tokens == null)
+ {
+ tokens = new ArrayList<>();
+
+ // Preserve original behaviour from RuntimeExec.
+ if (str.indexOf('\'') == -1 && str.indexOf('"') == -1)
+ {
+ // Contains no quotes.
+ for (StringTokenizer standardTokenizer = new StringTokenizer(
+ str); standardTokenizer.hasMoreTokens(); )
+ {
+ tokens.add(standardTokenizer.nextToken());
+ }
+ }
+ else
+ {
+ // There are either single or double quotes or both.
+ // So we need to identify the quoted regions within the string.
+ List> quotedRegions = new ArrayList<>();
+
+ for (Pair next = identifyNextQuotedRegion(str, 0); next != null; )
+ {
+ quotedRegions.add(next);
+ next = identifyNextQuotedRegion(str, next.getSecond() + 1);
+ }
+
+ // Now we've got a List of index pairs identifying the quoted regions.
+ // We need to get substrings of quoted and unquoted blocks, whilst maintaining order.
+ List substrings = getSubstrings(str, quotedRegions);
+
+ for (Substring r : substrings)
+ {
+ tokens.addAll(r.getTokens());
+ }
+ }
+ }
+
+ return this.tokens;
+ }
+
+ /**
+ * The substrings will be a list of quoted and unquoted substrings.
+ * The unquoted ones need to be further tokenized in the normal way.
+ * The quoted ones must not be tokenized, but need their quotes stripped off.
+ */
+ private List getSubstrings(String str,
+ List> quotedRegionIndices)
+ {
+ List result = new ArrayList<>();
+
+ int cursorPosition = 0;
+ for (Pair nextQuotedRegionIndices : quotedRegionIndices)
+ {
+ if (cursorPosition < nextQuotedRegionIndices.getFirst())
+ {
+ int startIndexOfNextQuotedRegion = nextQuotedRegionIndices.getFirst() - 1;
+ result.add(new UnquotedSubstring(
+ str.substring(cursorPosition, startIndexOfNextQuotedRegion)));
+ }
+ result.add(new QuotedSubstring(str.substring(nextQuotedRegionIndices.getFirst(),
+ nextQuotedRegionIndices.getSecond())));
+ cursorPosition = nextQuotedRegionIndices.getSecond();
+ }
+
+ // We've processed all the quoted regions, but there may be a final unquoted region
+ if (cursorPosition < str.length() - 1)
+ {
+ result.add(new UnquotedSubstring(str.substring(cursorPosition, str.length() - 1)));
+ }
+
+ return result;
+ }
+
+ private Pair identifyNextQuotedRegion(String str, int startingIndex)
+ {
+ int indexOfNextSingleQuote = str.indexOf('\'', startingIndex);
+ int indexOfNextDoubleQuote = str.indexOf('"', startingIndex);
+
+ if (indexOfNextSingleQuote == -1 && indexOfNextDoubleQuote == -1)
+ {
+ // If there are no more quoted regions
+ return null;
+ }
+ else if (indexOfNextSingleQuote > -1 && indexOfNextDoubleQuote > -1)
+ {
+ // If there are both single and double quotes in the remainder of the string
+ // Then select the closest quote.
+ int indexOfNextQuote = Math.min(indexOfNextSingleQuote, indexOfNextDoubleQuote);
+ char quoteChar = str.charAt(indexOfNextQuote);
+
+ return findIndexOfClosingQuote(str, indexOfNextQuote, quoteChar);
+ }
+ else
+ {
+ // Only one of the quote characters is present.
+
+ int indexOfNextQuote = Math.max(indexOfNextSingleQuote, indexOfNextDoubleQuote);
+ char quoteChar = str.charAt(indexOfNextQuote);
+
+ return findIndexOfClosingQuote(str, indexOfNextQuote, quoteChar);
+ }
+ }
+
+ private Pair findIndexOfClosingQuote(String str, int indexOfStartingQuote,
+ char quoteChar)
+ {
+ // So we know which type of quote char we're dealing with. Either ' or ".
+ // Now we need to find the closing quote.
+ int indexAfterClosingQuote = str.indexOf(quoteChar,
+ indexOfStartingQuote + 1) + 1; // + 1 to search after opening quote. + 1 to give result including closing quote.
+
+ if (indexAfterClosingQuote == 0) // -1 + 1
+ {
+ // If no closing quote.
+ throw new IllegalArgumentException("No closing " + quoteChar + "quote in" + str);
+ }
+
+ return new Pair<>(indexOfStartingQuote, indexAfterClosingQuote);
+ }
+
+ /**
+ * Utility interface for a substring in a parameter string.
+ */
+ public interface Substring
+ {
+ /**
+ * Gets all the tokens in a parameter string.
+ */
+ List getTokens();
+ }
+
+ /**
+ * A substring that is not surrounded by (single or double) quotes.
+ */
+ public class UnquotedSubstring implements Substring
+ {
+ private final String regionString;
+
+ public UnquotedSubstring(String str)
+ {
+ this.regionString = str;
+ }
+
+ public List getTokens()
+ {
+ StringTokenizer t = new StringTokenizer(regionString);
+ List result = new ArrayList<>();
+ while (t.hasMoreTokens())
+ {
+ result.add(t.nextToken());
+ }
+ return result;
+ }
+
+ public String toString()
+ {
+ return UnquotedSubstring.class.getSimpleName() + ": '" + regionString + '\'';
+ }
+ }
+
+ /**
+ * A substring that is surrounded by (single or double) quotes.
+ */
+ public class QuotedSubstring implements Substring
+ {
+ private final String regionString;
+
+ public QuotedSubstring(String str)
+ {
+ this.regionString = str;
+ }
+
+ public List getTokens()
+ {
+ String stringWithoutQuotes = regionString.substring(1, regionString.length() - 1);
+ return singletonList(stringWithoutQuotes);
+ }
+
+ public String toString()
+ {
+ return QuotedSubstring.class.getSimpleName() + ": '" + regionString + '\'';
+ }
+ }
+
+ public static final class Pair implements Serializable
+ {
+ private static final long serialVersionUID = -7406248421185630612L;
+
+ /**
+ * The first member of the pair.
+ */
+ private F first;
+
+ /**
+ * The second member of the pair.
+ */
+ private S second;
+
+ /**
+ * Make a new one.
+ *
+ * @param first The first member.
+ * @param second The second member.
+ */
+ public Pair(F first, S second)
+ {
+ this.first = first;
+ this.second = second;
+ }
+
+ /**
+ * Get the first member of the tuple.
+ *
+ * @return The first member.
+ */
+ public final F getFirst()
+ {
+ return first;
+ }
+
+ /**
+ * Get the second member of the tuple.
+ *
+ * @return The second member.
+ */
+ public final S getSecond()
+ {
+ return second;
+ }
+
+ public final void setFirst(F first)
+ {
+ this.first = first;
+ }
+
+ public final void setSecond(S second)
+ {
+ this.second = second;
+ }
+
+ @Override public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Pair, ?> pair = (Pair, ?>) o;
+ return Objects.equals(first, pair.first) &&
+ Objects.equals(second, pair.second);
+ }
+
+ @Override public int hashCode()
+ {
+ return Objects.hash(first, second);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "(" + first + ", " + second + ")";
+ }
+ }
+}
+
diff --git a/t-engine-base/src/main/java/org/alfresco/transform/base/executors/JavaExecutor.java b/t-engine-base/src/main/java/org/alfresco/transform/base/executors/JavaExecutor.java
new file mode 100644
index 00000000..545a2f27
--- /dev/null
+++ b/t-engine-base/src/main/java/org/alfresco/transform/base/executors/JavaExecutor.java
@@ -0,0 +1,40 @@
+/*
+ * #%L
+ * Alfresco Transform Core
+ * %%
+ * Copyright (C) 2005 - 2022 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.transform.base.executors;
+
+import java.io.File;
+
+/**
+ * Basic interface for executing transformations inside Java/JVM.
+ *
+ * @author Cezar Leahu
+ * @author adavis
+ */
+public interface JavaExecutor extends Transformer
+{
+ void call(File sourceFile, File targetFile, String... args) throws Exception;
+}
diff --git a/t-engine-base/src/main/java/org/alfresco/transform/base/executors/RuntimeExec.java b/t-engine-base/src/main/java/org/alfresco/transform/base/executors/RuntimeExec.java
new file mode 100644
index 00000000..3363f646
--- /dev/null
+++ b/t-engine-base/src/main/java/org/alfresco/transform/base/executors/RuntimeExec.java
@@ -0,0 +1,986 @@
+/*
+ * #%L
+ * Alfresco Transform Core
+ * %%
+ * Copyright (C) 2005 - 2022 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.transform.base.executors;
+
+import static java.util.Collections.emptyMap;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.nio.charset.UnsupportedCharsetException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * DUPLICATED FROM *alfresco-core*.
+ *
+ * This acts as a session similar to the java.lang.Process, but
+ * logs the system standard and error streams.
+ *
+ * The bean can be configured to execute a command directly, or be given a map
+ * of commands keyed by the os.name Java system property. In this map,
+ * the default key that is used when no match is found is the
+ * {@link #KEY_OS_DEFAULT *} key.
+ *
+ * Use the {@link #setProcessDirectory(String) processDirectory} property to change the default location
+ * from which the command executes. The process's environment can be configured using the
+ * {@link #setProcessProperties(Map) processProperties} property.
+ *
+ * Commands may use placeholders, e.g.
+ *
+ * find
+ * -name
+ * ${filename}
+ *
+ * The filename property will be substituted for any supplied value prior to
+ * each execution of the command. Currently, no checks are made to get or check the
+ * properties contained within the command string. It is up to the client code to
+ * dynamically extract the properties required if the required properties are not
+ * known up front.
+ *
+ * Sometimes, a variable may contain several arguments. . In this case, the arguments
+ * need to be tokenized using a standard StringTokenizer. To force tokenization
+ * of a value, use:
+ *
+ * SPLIT:${userArgs}
+ *
+ * You should not use this just to split up arguments that are known to require tokenization
+ * up front. The SPLIT: directive works for the entire argument and will not do anything
+ * if it is not at the beginning of the argument. Do not use SPLIT: to break up arguments
+ * that are fixed, so avoid doing this:
+ *
+ * SPLIT:ls -lih
+ *
+ * Instead, break the command up explicitly:
+ *
+ * ls
+ * -lih
+ *
+ *
+ * Tokenization of quoted parameter values is handled by ExecParameterTokenizer, which
+ * describes the support in more detail.
+ *
+ * @author Derek Hulley
+ */
+public class RuntimeExec
+{
+ private static final Logger logger = LoggerFactory.getLogger(RuntimeExec.class);
+
+ /**
+ * the key to use when specifying a command for any other OS: *
+ */
+ private static final String KEY_OS_DEFAULT = "*";
+
+ private static final String KEY_OS_NAME = "os.name";
+ private static final int BUFFER_SIZE = 1024;
+ private static final String VAR_OPEN = "${";
+ private static final String VAR_CLOSE = "}";
+ private static final String DIRECTIVE_SPLIT = "SPLIT:";
+
+ private String[] command;
+ private Charset charset = Charset.defaultCharset();
+ private boolean waitForCompletion = true;
+ private Map defaultProperties = emptyMap();
+ private String[] processProperties;
+ private File processDirectory;
+ private final Set errCodes;
+ private final Timer timer = new Timer(true);
+
+ /**
+ * Default constructor. Initialize this instance by setting individual properties.
+ */
+ public RuntimeExec()
+ {
+ // set default error codes
+ errCodes = new HashSet<>(2);
+ errCodes.add(1);
+ errCodes.add(2);
+ }
+
+ public String toString()
+ {
+ final StringBuilder sb = new StringBuilder(256);
+ sb.append("RuntimeExec:\n").append(" command: ");
+ if (command == null)
+ {
+ // command is 'null', so there's nothing to toString
+ sb.append("'null'\n");
+ }
+ else
+ {
+ for (String cmdStr : command)
+ {
+ sb.append(cmdStr).append(" ");
+ }
+ sb.append("\n");
+ }
+ sb.append(" env props: ").append(Arrays.toString(processProperties)).append("\n")
+ .append(" dir: ").append(processDirectory).append("\n")
+ .append(" os: ").append(System.getProperty(KEY_OS_NAME)).append("\n");
+ return sb.toString();
+ }
+
+ /**
+ * Set the command to execute regardless of operating system
+ *
+ * @param command an array of strings representing the command (first entry) and arguments
+ * @since 3.0
+ */
+ public void setCommand(String[] command)
+ {
+ this.command = command;
+ }
+
+ /**
+ * Sets the assumed charset of OUT and ERR streams generated by the executed command.
+ * This defaults to the system default charset: {@link Charset#defaultCharset()}.
+ *
+ * @param charsetCode a supported character set code
+ * @throws UnsupportedCharsetException if the characterset code is not recognised by Java
+ */
+ public void setCharset(String charsetCode)
+ {
+ this.charset = Charset.forName(charsetCode);
+ }
+
+ /**
+ * Set whether to wait for completion of the command or not. If there is no wait for completion,
+ * then the return value of out and err buffers cannot be relied upon as the
+ * command may still be in progress. Failure is therefore not possible unless the calling thread
+ * waits for execution.
+ *
+ * @param waitForCompletion true (default) is to wait for the command to exit,
+ * or false to just return an exit code of 0 and whatever
+ * output is available at that point.
+ * @since 2.1
+ */
+ public void setWaitForCompletion(boolean waitForCompletion)
+ {
+ this.waitForCompletion = waitForCompletion;
+ }
+
+ /**
+ * Supply a choice of commands to execute based on a mapping from the os.name system
+ * property to the command to execute. The {@link #KEY_OS_DEFAULT *} key can be used
+ * to get a command where there is not direct match to the operating system key.
+ *
+ * Each command is an array of strings, the first of which represents the command and all subsequent
+ * entries in the array represent the arguments. All elements of the array will be checked for
+ * the presence of any substitution parameters (e.g. '{dir}'). The parameters can be set using the
+ * {@link #setDefaultProperties(Map) defaults} or by passing the substitution values into the
+ * {@link #execute(Map)} command.
+ *
+ * If parameters passed may be multiple arguments, or if the values provided in the map are themselves
+ * collections of arguments (not recommended), then prefix the value with SPLIT: to ensure that
+ * the value is tokenized before being passed to the command. Any values that are not split, will be
+ * passed to the command as single arguments. For example:
+ * 'SPLIT: dir . ..' becomes 'dir', '.' and '..'.
+ * 'SPLIT: dir ${path}' (if path is '. ..') becomes 'dir', '.' and '..'.
+ * The splitting occurs post-subtitution. Where the arguments are known, it is advisable to avoid
+ * SPLIT:.
+ *
+ * @param commandsByOS a map of command string arrays, keyed by operating system names
+ * @see #setDefaultProperties(Map)
+ * @since 3.0
+ */
+ public void setCommandsAndArguments(Map commandsByOS)
+ {
+ // get the current OS
+ String serverOs = System.getProperty(KEY_OS_NAME);
+ // attempt to find a match
+ String[] command = commandsByOS.get(serverOs);
+ if (command == null)
+ {
+ // go through the commands keys, looking for one that matches by regular expression matching
+ for (String osName : commandsByOS.keySet())
+ {
+ // Ignore * options. It is dealt with later.
+ if (osName.equals(KEY_OS_DEFAULT))
+ {
+ continue;
+ }
+ // Do regex match
+ if (serverOs.matches(osName))
+ {
+ command = commandsByOS.get(osName);
+ break;
+ }
+ }
+ // if there is still no command, then check for the wildcard
+ if (command == null)
+ {
+ command = commandsByOS.get(KEY_OS_DEFAULT);
+ }
+ }
+ // check
+ if (command == null)
+ {
+ throw new RuntimeException(
+ "No command found for OS " + serverOs + " or '" + KEY_OS_DEFAULT + "': \n" +
+ " commands: " + commandsByOS);
+ }
+ this.command = command;
+ }
+
+ /**
+ * Supply a choice of commands to execute based on a mapping from the os.name system
+ * property to the command to execute. The {@link #KEY_OS_DEFAULT *} key can be used
+ * to get a command where there is not direct match to the operating system key.
+ *
+ * @param commandsByOS a map of command string keyed by operating system names
+ * @deprecated Use {@link #setCommandsAndArguments(Map)}
+ */
+ public void setCommandMap(Map commandsByOS)
+ {
+ // This is deprecated, so issue a warning
+ logger.warn(
+ "The bean RuntimeExec property 'commandMap' has been deprecated;" +
+ " use 'commandsAndArguments' instead. See https://issues.alfresco.com/jira/browse/ETHREEOH-579.");
+ Map fixed = new LinkedHashMap<>();
+ for (Map.Entry entry : commandsByOS.entrySet())
+ {
+ String os = entry.getKey();
+ String unparsedCmd = entry.getValue();
+ StringTokenizer tokenizer = new StringTokenizer(unparsedCmd);
+ String[] cmd = new String[tokenizer.countTokens()];
+ for (int i = 0; i < cmd.length; i++)
+ {
+ cmd[i] = tokenizer.nextToken();
+ }
+ fixed.put(os, cmd);
+ }
+ setCommandsAndArguments(fixed);
+ }
+
+ /**
+ * Set the default command-line properties to use when executing the command.
+ * These are properties that substitute variables defined in the command string itself.
+ * Properties supplied during execution will overwrite the default properties.
+ *
+ * null properties will be treated as an empty string for substitution
+ * purposes.
+ *
+ * @param defaultProperties property values
+ */
+ public void setDefaultProperties(Map defaultProperties)
+ {
+ this.defaultProperties = defaultProperties;
+ }
+
+ /**
+ * Set additional runtime properties (environment properties) that will used
+ * by the executing process.
+ *
+ * Any keys or properties that start and end with ${...} will be removed on the assumption
+ * that these are unset properties. null values are translated to empty strings.
+ * All keys and values are trimmed of leading and trailing whitespace.
+ *
+ * @param processProperties Runtime process properties
+ * @see Runtime#exec(String, String[], java.io.File)
+ */
+ public void setProcessProperties(Map processProperties)
+ {
+ ArrayList processPropList = new ArrayList<>(processProperties.size());
+ boolean hasPath = false;
+ String systemPath = System.getenv("PATH");
+ for (Map.Entry entry : processProperties.entrySet())
+ {
+ String key = entry.getKey();
+ String value = entry.getValue();
+ if (key == null)
+ {
+ continue;
+ }
+ if (value == null)
+ {
+ value = "";
+ }
+ key = key.trim();
+ value = value.trim();
+ if (key.startsWith(VAR_OPEN) && key.endsWith(VAR_CLOSE))
+ {
+ continue;
+ }
+ if (value.startsWith(VAR_OPEN) && value.endsWith(VAR_CLOSE))
+ {
+ continue;
+ }
+ // If a path is specified, prepend it to the existing path
+ if (key.equals("PATH"))
+ {
+ if (systemPath != null && systemPath.length() > 0)
+ {
+ processPropList.add(key + "=" + value + File.pathSeparator + systemPath);
+ }
+ else
+ {
+ processPropList.add(key + "=" + value);
+ }
+ hasPath = true;
+ }
+ else
+ {
+ processPropList.add(key + "=" + value);
+ }
+ }
+ // If a path was not specified, inherit the current one
+ if (!hasPath && systemPath != null && systemPath.length() > 0)
+ {
+ processPropList.add("PATH=" + systemPath);
+ }
+ this.processProperties = processPropList.toArray(new String[0]);
+ }
+
+ /**
+ * Adds a property to existed processProperties.
+ * Property should not be null or empty.
+ * If property with the same value already exists then no change is made.
+ * If property exists with a different value then old value is replaced with the new one.
+ *
+ * @param name - property name
+ * @param value - property value
+ */
+ public void setProcessProperty(String name, String value)
+ {
+ boolean set = false;
+
+ if (name == null || value == null)
+ {
+ return;
+ }
+
+ name = name.trim();
+ value = value.trim();
+
+ if (name.isEmpty() || value.isEmpty())
+ {
+ return;
+ }
+
+ String property = name + "=" + value;
+
+ for (String prop : this.processProperties)
+ {
+ if (prop.equals(property))
+ {
+ set = true;
+ break;
+ }
+
+ if (prop.startsWith(name))
+ {
+ String oldValue = prop.split("=")[1];
+ prop.replace(oldValue, value);
+ set = true;
+ }
+ }
+
+ if (!set)
+ {
+ String[] existedProperties = this.processProperties;
+ int epl = existedProperties.length;
+ String[] newProperties = Arrays.copyOf(existedProperties, epl + 1);
+ newProperties[epl] = property;
+ this.processProperties = newProperties;
+ }
+ }
+
+ /**
+ * Set the runtime location from which the command is executed.
+ *
+ * If the value is an unsubsititued variable (${...}) then it is ignored.
+ * If the location is not visible at the time of setting, a warning is issued only.
+ *
+ * @param processDirectory the runtime location from which to execute the command
+ */
+ public void setProcessDirectory(String processDirectory)
+ {
+ if (processDirectory.startsWith(VAR_OPEN) && processDirectory.endsWith(VAR_CLOSE))
+ {
+ this.processDirectory = null;
+ }
+ else
+ {
+ this.processDirectory = new File(processDirectory);
+ if (!this.processDirectory.exists())
+ {
+ logger.warn(
+ "The runtime process directory is not visible when setting property " +
+ "'processDirectory': \n{}", this);
+ }
+ }
+ }
+
+ /**
+ * A comma or space separated list of values that, if returned by the executed command,
+ * indicate an error value. This defaults to "1, 2".
+ *
+ * @param errCodesStr the error codes for the execution
+ */
+ public void setErrorCodes(String errCodesStr)
+ {
+ errCodes.clear();
+ StringTokenizer tokenizer = new StringTokenizer(errCodesStr, " ,");
+ while (tokenizer.hasMoreElements())
+ {
+ String errCodeStr = tokenizer.nextToken();
+ // attempt to convert it to an integer
+ try
+ {
+ int errCode = Integer.parseInt(errCodeStr);
+ this.errCodes.add(errCode);
+ }
+ catch (NumberFormatException e)
+ {
+ throw new RuntimeException(
+ "Property 'errorCodes' must be comma-separated list of integers: " + errCodesStr);
+ }
+ }
+ }
+
+ /**
+ * Executes the command using the default properties
+ *
+ * @see #execute(Map)
+ */
+ public ExecutionResult execute()
+ {
+ return execute(defaultProperties);
+ }
+
+ /**
+ * Executes the statement that this instance was constructed with.
+ *
+ * @param properties the properties that the command might be executed with.
+ * null properties will be treated as an empty string for substitution
+ * purposes.
+ * @return Returns the full execution results
+ */
+ public ExecutionResult execute(Map properties)
+ {
+ return execute(properties, -1);
+ }
+
+ /**
+ * Executes the statement that this instance was constructed with an optional
+ * timeout after which the command is asked to
+ *
+ * @param properties the properties that the command might be executed with.
+ * null properties will be treated as an empty string for substitution
+ * purposes.
+ * @param timeoutMs a timeout after which {@link Process#destroy()} is called.
+ * ignored if less than or equal to zero. Note this method does not guarantee
+ * to terminate the process (it is not a kill -9).
+ * @return Returns the full execution results
+ */
+ public ExecutionResult execute(Map properties, final long timeoutMs)
+ {
+ int defaultFailureExitValue = errCodes.size() > 0 ? ((Integer) errCodes.toArray()[0]) : 1;
+
+ // check that the command has been set
+ if (command == null)
+ {
+ throw new RuntimeException("Runtime command has not been set: \n" + this);
+ }
+
+ // create the properties
+ Runtime runtime = Runtime.getRuntime();
+ Process process;
+ String[] commandToExecute = null;
+ try
+ {
+ // execute the command with full property replacement
+ commandToExecute = getCommand(properties);
+ final Process thisProcess = runtime.exec(commandToExecute, processProperties,
+ processDirectory);
+ process = thisProcess;
+ if (timeoutMs > 0)
+ {
+ final String[] command = commandToExecute;
+ timer.schedule(new TimerTask()
+ {
+ @Override
+ public void run()
+ {
+ // Only try to kill the process if it is still running
+ try
+ {
+ thisProcess.exitValue();
+ }
+ catch (IllegalThreadStateException stillRunning)
+ {
+ logger.debug(
+ "Process has taken too long ({} seconds). Killing process {}",
+ timeoutMs / 1000, Arrays.deepToString(command));
+ }
+ }
+ }, timeoutMs);
+ }
+ }
+ catch (IOException e)
+ {
+ // The process could not be executed here, so just drop out with an appropriate error state
+ String execOut = "";
+ String execErr = e.getMessage();
+ ExecutionResult result = new ExecutionResult(null, commandToExecute, errCodes,
+ defaultFailureExitValue, execOut, execErr);
+ logFullEnvironmentDump(result);
+ return result;
+ }
+
+ // create the stream gobblers
+ InputStreamReaderThread stdOutGobbler = new InputStreamReaderThread(
+ process.getInputStream(), charset);
+ InputStreamReaderThread stdErrGobbler = new InputStreamReaderThread(
+ process.getErrorStream(), charset);
+
+ // start gobbling
+ stdOutGobbler.start();
+ stdErrGobbler.start();
+
+ // wait for the process to finish
+ int exitValue = 0;
+ try
+ {
+ if (waitForCompletion)
+ {
+ exitValue = process.waitFor();
+ }
+ }
+ catch (InterruptedException e)
+ {
+ // process was interrupted - generate an error message
+ stdErrGobbler.addToBuffer(e.toString());
+ exitValue = defaultFailureExitValue;
+ }
+
+ if (waitForCompletion)
+ {
+ // ensure that the stream gobblers get to finish
+ stdOutGobbler.waitForCompletion();
+ stdErrGobbler.waitForCompletion();
+ }
+
+ // get the stream values
+ String execOut = stdOutGobbler.getBuffer();
+ String execErr = stdErrGobbler.getBuffer();
+
+ // construct the return value
+ ExecutionResult result = new ExecutionResult(process, commandToExecute, errCodes, exitValue,
+ execOut, execErr);
+
+ // done
+ logFullEnvironmentDump(result);
+ return result;
+ }
+
+ /**
+ * Dump the full environment in debug mode
+ */
+ private void logFullEnvironmentDump(ExecutionResult result)
+ {
+ if (logger.isTraceEnabled())
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append(result);
+
+ // Environment variables modified by Alfresco
+ if (processProperties != null && processProperties.length > 0)
+ {
+ sb.append("\n modified environment: ");
+ for (String property : processProperties)
+ {
+ sb.append("\n ");
+ sb.append(property);
+ }
+ }
+
+ // Dump the full environment
+ sb.append("\n existing environment: ");
+ Map envVariables = System.getenv();
+ for (Map.Entry entry : envVariables.entrySet())
+ {
+ String name = entry.getKey();
+ String value = entry.getValue();
+ sb.append("\n ");
+ sb.append(name).append("=").append(value);
+ }
+
+ logger.trace(sb.toString());
+ }
+ logger.debug("Result: " + result.toString());
+
+ // close output stream (connected to input stream of native subprocess)
+ }
+
+ /**
+ * @return Returns the command that will be executed if no additional properties
+ * were to be supplied
+ */
+ public String[] getCommand()
+ {
+ return getCommand(defaultProperties);
+ }
+
+ /**
+ * Get the command that will be executed post substitution.
+ *
+ * null properties will be treated as an empty string for substitution
+ * purposes.
+ *
+ * @param properties the properties that the command might be executed with
+ * @return Returns the command that will be executed should the additional properties
+ * be supplied
+ */
+ public String[] getCommand(Map properties)
+ {
+ Map execProperties;
+ if (properties == defaultProperties)
+ {
+ // we are just using the default properties
+ execProperties = defaultProperties;
+ }
+ else
+ {
+ execProperties = new HashMap<>(defaultProperties);
+ // overlay the supplied properties
+ execProperties.putAll(properties);
+ }
+ // Perform the substitution for each element of the command
+ ArrayList adjustedCommandElements = new ArrayList<>(20);
+ for (String s : command)
+ {
+ StringBuilder sb = new StringBuilder(s);
+ for (Map.Entry entry : execProperties.entrySet())
+ {
+ String key = entry.getKey();
+ String value = entry.getValue();
+ // ignore null
+ if (value == null)
+ {
+ value = "";
+ }
+ // progressively replace the property in the command
+ key = (VAR_OPEN + key + VAR_CLOSE);
+ int index = sb.indexOf(key);
+ while (index > -1)
+ {
+ // replace
+ sb.replace(index, index + key.length(), value);
+ // get the next one
+ index = sb.indexOf(key, index + 1);
+ }
+ }
+ String adjustedValue = sb.toString();
+ // Now SPLIT: it
+ if (adjustedValue.startsWith(DIRECTIVE_SPLIT))
+ {
+ String unsplitAdjustedValue = sb.substring(DIRECTIVE_SPLIT.length());
+
+ // There may be quoted arguments here (see ALF-7482)
+ ExecParameterTokenizer quoteAwareTokenizer = new ExecParameterTokenizer(
+ unsplitAdjustedValue);
+ List tokens = quoteAwareTokenizer.getAllTokens();
+ adjustedCommandElements.addAll(tokens);
+ }
+ else
+ {
+ adjustedCommandElements.add(adjustedValue);
+ }
+ }
+ // done
+ return adjustedCommandElements.toArray(new String[0]);
+ }
+
+ /**
+ * Object to carry the results of an execution to the caller.
+ *
+ * @author Derek Hulley
+ */
+ public static class ExecutionResult
+ {
+ private final Process process;
+ private final String[] command;
+ private final Set errCodes;
+ private final int exitValue;
+ private final String stdOut;
+ private final String stdErr;
+
+ /**
+ * @param process the process attached to Java - null is allowed
+ */
+ private ExecutionResult(
+ final Process process,
+ final String[] command,
+ final Set errCodes,
+ final int exitValue,
+ final String stdOut,
+ final String stdErr)
+ {
+ this.process = process;
+ this.command = command;
+ this.errCodes = errCodes;
+ this.exitValue = exitValue;
+ this.stdOut = stdOut;
+ this.stdErr = stdErr;
+ }
+
+ @Override
+ public String toString()
+ {
+ String out = stdOut.length() > 250 ? stdOut.substring(0, 250) : stdOut;
+ String err = stdErr.length() > 250 ? stdErr.substring(0, 250) : stdErr;
+
+ StringBuilder sb = new StringBuilder(128);
+ sb.append("Execution result: \n")
+ .append(" os: ").append(System.getProperty(KEY_OS_NAME)).append("\n")
+ .append(" command: ");
+ appendCommand(sb, command).append("\n")
+ .append(" succeeded: ").append(getSuccess()).append("\n")
+ .append(" exit code: ").append(exitValue).append("\n")
+ .append(" out: ").append(out).append("\n")
+ .append(" err: ").append(err);
+ return sb.toString();
+ }
+
+ /**
+ * Appends the command in a form that make running from the command line simpler.
+ * It is not a real attempt at making a command given all the operating system
+ * and shell options, but makes copy, paste and edit a bit simpler.
+ */
+ private StringBuilder appendCommand(StringBuilder sb, String[] command)
+ {
+ boolean arg = false;
+ for (String element : command)
+ {
+ if (element == null)
+ {
+ continue;
+ }
+
+ if (arg)
+ {
+ sb.append(' ');
+ }
+ else
+ {
+ arg = true;
+ }
+
+ boolean escape = element.indexOf(' ') != -1 || element.indexOf('>') != -1;
+ if (escape)
+ {
+ sb.append("\"");
+ }
+ sb.append(element);
+ if (escape)
+ {
+ sb.append("\"");
+ }
+ }
+ return sb;
+ }
+
+ /**
+ * A helper method to force a kill of the process that generated this result. This is
+ * useful in cases where the process started is not expected to exit, or doesn't exit
+ * quickly. If the {@linkplain RuntimeExec#setWaitForCompletion(boolean) "wait for completion"}
+ * flag is false then the process may still be running when this result is returned.
+ *
+ * @return true if the process was killed, otherwise false
+ */
+ public boolean killProcess()
+ {
+ if (process == null)
+ {
+ return true;
+ }
+ try
+ {
+ process.destroy();
+ return true;
+ }
+ catch (Throwable e)
+ {
+ logger.warn(e.getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * @param exitValue the command exit value
+ * @return Returns true if the code is a listed failure code
+ * @see #setErrorCodes(String)
+ */
+ private boolean isFailureCode(int exitValue)
+ {
+ return errCodes.contains(exitValue);
+ }
+
+ /**
+ * @return Returns true if the command was deemed to be successful according to the
+ * failure codes returned by the execution.
+ */
+ public boolean getSuccess()
+ {
+ return !isFailureCode(exitValue);
+ }
+
+ public int getExitValue()
+ {
+ return exitValue;
+ }
+
+ public String getStdOut()
+ {
+ return stdOut;
+ }
+
+ public String getStdErr()
+ {
+ return stdErr;
+ }
+ }
+
+ /**
+ * Gobbles an InputStream and writes it into a
+ * StringBuffer
+ *
+ * The reading of the input stream is buffered.
+ */
+ public static class InputStreamReaderThread extends Thread
+ {
+ private final InputStream is;
+ private final Charset charset;
+ private final StringBuffer buffer; // we require the synchronization
+ private boolean completed;
+
+ /**
+ * @param is an input stream to read - it will be wrapped in a buffer
+ * for reading
+ */
+ public InputStreamReaderThread(InputStream is, Charset charset)
+ {
+ super();
+ setDaemon(true); // must not hold up the VM if it is terminating
+ this.is = is;
+ this.charset = charset;
+ this.buffer = new StringBuffer(BUFFER_SIZE);
+ this.completed = false;
+ }
+
+ public synchronized void run()
+ {
+ completed = false;
+
+ byte[] bytes = new byte[BUFFER_SIZE];
+ try (InputStream tempIs = new BufferedInputStream(is, BUFFER_SIZE))
+ {
+ int count = -2;
+ while (count != -1)
+ {
+ // do we have something previously read?
+ if (count > 0)
+ {
+ String toWrite = new String(bytes, 0, count, charset.name());
+ buffer.append(toWrite);
+ }
+ // read the next set of bytes
+ count = tempIs.read(bytes);
+ }
+ // done
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException("Unable to read stream", e);
+ }
+ finally
+ {
+ // The thread has finished consuming the stream
+ completed = true;
+ // Notify waiters
+ this.notifyAll(); // Note: Method is synchronized
+ }
+ }
+
+ /**
+ * Waits for the run to complete.
+ *
+ * Remember to start the thread first
+ */
+ public synchronized void waitForCompletion()
+ {
+ while (!completed)
+ {
+ try
+ {
+ // release our lock and wait a bit
+ this.wait(1000L); // 200 ms
+ }
+ catch (InterruptedException ignore)
+ {
+ }
+ }
+ }
+
+ /**
+ * @param msg the message to add to the buffer
+ */
+ public void addToBuffer(String msg)
+ {
+ buffer.append(msg);
+ }
+
+ public boolean isComplete()
+ {
+ return completed;
+ }
+
+ /**
+ * @return Returns the current state of the buffer
+ */
+ public String getBuffer()
+ {
+ return buffer.toString();
+ }
+ }
+}
+
diff --git a/t-engine-base/src/main/java/org/alfresco/transform/base/executors/Transformer.java b/t-engine-base/src/main/java/org/alfresco/transform/base/executors/Transformer.java
new file mode 100644
index 00000000..73751b94
--- /dev/null
+++ b/t-engine-base/src/main/java/org/alfresco/transform/base/executors/Transformer.java
@@ -0,0 +1,130 @@
+package org.alfresco.transform.base.executors;
+
+/*
+ * #%L
+ * Alfresco Transform Core
+ * %%
+ * Copyright (C) 2005 - 2022 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+
+import org.alfresco.transform.common.TransformException;
+
+import java.io.File;
+import java.util.Map;
+
+import static org.alfresco.transform.common.Mimetype.MIMETYPE_METADATA_EMBED;
+import static org.alfresco.transform.common.Mimetype.MIMETYPE_METADATA_EXTRACT;
+import static org.alfresco.transform.base.util.RequestParamMap.TRANSFORM_NAME_PARAMETER;
+import static org.springframework.http.HttpStatus.BAD_REQUEST;
+import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
+
+/**
+ * Basic interface for executing transforms and metadata extract or embed actions.
+ *
+ * @author adavis
+ */
+public interface Transformer
+{
+ /**
+ * @return A unique transformer id,
+ *
+ */
+ String getTransformerId();
+
+ default void transform(String sourceMimetype, String targetMimetype, Map transformOptions,
+ File sourceFile, File targetFile) throws TransformException {
+ final String transformName = transformOptions.remove(TRANSFORM_NAME_PARAMETER);
+ transformExtractOrEmbed(transformName, sourceMimetype, targetMimetype, transformOptions, sourceFile, targetFile);
+ }
+
+ default void transformExtractOrEmbed(String transformName, String sourceMimetype, String targetMimetype,
+ Map transformOptions,
+ File sourceFile, File targetFile) throws TransformException
+ {
+ try
+ {
+ if (MIMETYPE_METADATA_EXTRACT.equals(targetMimetype))
+ {
+ extractMetadata(transformName, sourceMimetype, targetMimetype, transformOptions, sourceFile, targetFile);
+ }
+ else if (MIMETYPE_METADATA_EMBED.equals(targetMimetype))
+ {
+ embedMetadata(transformName, sourceMimetype, targetMimetype, transformOptions, sourceFile, targetFile);
+ }
+ else
+ {
+ transform(transformName, sourceMimetype, targetMimetype, transformOptions, sourceFile, targetFile);
+ }
+ }
+ catch (TransformException e)
+ {
+ throw e;
+ }
+ catch (IllegalArgumentException e)
+ {
+ throw new TransformException(BAD_REQUEST.value(), getMessage(e), e);
+ }
+ catch (Exception e)
+ {
+ throw new TransformException(INTERNAL_SERVER_ERROR.value(), getMessage(e), e);
+ }
+ if (!targetFile.exists())
+ {
+ throw new TransformException(INTERNAL_SERVER_ERROR.value(),
+ "Transformer failed to create an output file. Target file does not exist.");
+ }
+ if (sourceFile.length() > 0 && targetFile.length() == 0)
+ {
+ throw new TransformException(INTERNAL_SERVER_ERROR.value(),
+ "Transformer failed to create an output file. Target file is empty but source file was not empty.");
+ }
+ }
+
+ private static String getMessage(Exception e)
+ {
+ return e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage();
+ }
+
+ default void transform(String transformName, String sourceMimetype, String targetMimetype,
+ Map transformOptions,
+ File sourceFile, File targetFile) throws Exception
+ {
+ }
+
+ default void extractMetadata(String transformName, String sourceMimetype, String targetMimetype,
+ Map transformOptions,
+ File sourceFile, File targetFile) throws Exception
+ {
+ }
+
+ /**
+ * @deprecated The content repository has no non test embed metadata implementations.
+ * This code exists in case there are custom implementations, that need to be converted to T-Engines.
+ * It is simply a copy and paste from the content repository and has received limited testing.
+ */
+ default void embedMetadata(String transformName, String sourceMimetype, String targetMimetype,
+ Map transformOptions,
+ File sourceFile, File targetFile) throws Exception
+ {
+ }
+}
diff --git a/t-engine-base/src/main/java/org/alfresco/transform/base/fs/FileManager.java b/t-engine-base/src/main/java/org/alfresco/transform/base/fs/FileManager.java
new file mode 100644
index 00000000..270d423b
--- /dev/null
+++ b/t-engine-base/src/main/java/org/alfresco/transform/base/fs/FileManager.java
@@ -0,0 +1,249 @@
+/*
+ * #%L
+ * Alfresco Transform Core
+ * %%
+ * Copyright (C) 2005 - 2022 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.transform.base.fs;
+
+import static org.springframework.http.HttpHeaders.CONTENT_DISPOSITION;
+import static org.springframework.http.HttpStatus.BAD_REQUEST;
+import static org.springframework.http.HttpStatus.INSUFFICIENT_STORAGE;
+import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
+import static org.springframework.util.StringUtils.getFilename;
+import static org.springframework.util.StringUtils.getFilenameExtension;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.util.Arrays;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.alfresco.transform.common.ExtensionService;
+import org.alfresco.transform.common.TransformException;
+import org.alfresco.transform.base.logging.LogEntry;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.UrlResource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.util.UriUtils;
+
+public class FileManager
+{
+ public static final String SOURCE_FILE = "sourceFile";
+ public static final String TARGET_FILE = "targetFile";
+ private static final String FILENAME = "filename=";
+
+ public static File createTargetFile(HttpServletRequest request, String filename)
+ {
+ File file = buildFile(filename);
+ request.setAttribute(TARGET_FILE, file);
+ return file;
+ }
+
+ public static File buildFile(String filename)
+ {
+ filename = checkFilename(false, filename);
+ LogEntry.setTarget(filename);
+ return TempFileProvider.createTempFile("target_", "_" + filename);
+ }
+
+ public static void deleteFile(final File file) throws Exception
+ {
+ if (!file.delete())
+ {
+ throw new Exception("Failed to delete file");
+ }
+ }
+
+ private static String checkFilename(boolean source, String filename)
+ {
+ filename = getFilename(filename);
+ if (filename == null || filename.isEmpty())
+ {
+ String sourceOrTarget = source ? "source" : "target";
+ int statusCode = source ? BAD_REQUEST.value() : INTERNAL_SERVER_ERROR.value();
+ throw new TransformException(statusCode,
+ "The " + sourceOrTarget + " filename was not supplied");
+ }
+ return filename;
+ }
+
+ private static void save(MultipartFile multipartFile, File file)
+ {
+ try
+ {
+ Files.copy(multipartFile.getInputStream(), file.toPath(),
+ StandardCopyOption.REPLACE_EXISTING);
+ }
+ catch (IOException e)
+ {
+ throw new TransformException(INSUFFICIENT_STORAGE.value(),
+ "Failed to store the source file", e);
+ }
+ }
+
+ public static void save(Resource body, File file)
+ {
+ try
+ {
+ Files.copy(body.getInputStream(), file.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ }
+ catch (IOException e)
+ {
+ throw new TransformException(INSUFFICIENT_STORAGE.value(),
+ "Failed to store the source file", e);
+ }
+ }
+
+ private static Resource load(File file)
+ {
+ try
+ {
+ Resource resource = new UrlResource(file.toURI());
+ if (resource.exists() || resource.isReadable())
+ {
+ return resource;
+ }
+ else
+ {
+ throw new TransformException(INTERNAL_SERVER_ERROR.value(),
+ "Could not read the target file: " + file.getPath());
+ }
+ }
+ catch (MalformedURLException e)
+ {
+ throw new TransformException(INTERNAL_SERVER_ERROR.value(),
+ "The target filename was malformed: " + file.getPath(), e);
+ }
+ }
+
+ public static String getFilenameFromContentDisposition(HttpHeaders headers)
+ {
+ String filename = "";
+ String contentDisposition = headers.getFirst(CONTENT_DISPOSITION);
+ if (contentDisposition != null)
+ {
+ String[] strings = contentDisposition.split("; *");
+ filename = Arrays.stream(strings)
+ .filter(s -> s.startsWith(FILENAME))
+ .findFirst()
+ .map(s -> s.substring(FILENAME.length()))
+ .orElse("");
+ }
+ return filename;
+ }
+
+ public static String createTargetFileName(final String fileName, String sourceMimetype, String targetMimetype)
+ {
+ String targetExtension = ExtensionService.getExtensionForTargetMimetype(targetMimetype, sourceMimetype);
+ final String sourceFilename = getFilename(fileName);
+
+ if (sourceFilename == null || sourceFilename.isEmpty())
+ {
+ return null;
+ }
+
+ final String ext = getFilenameExtension(sourceFilename);
+ if (ext == null || ext.isEmpty())
+ {
+ return sourceFilename + '.' + targetExtension;
+ }
+ return sourceFilename.substring(0, sourceFilename.length() - ext.length() - 1) + '.' + targetExtension;
+ }
+
+ public static File createSourceFile(HttpServletRequest request, MultipartFile multipartFile)
+ {
+ String filename = multipartFile.getOriginalFilename();
+ long size = multipartFile.getSize();
+ filename = checkFilename(true, filename);
+ File file = TempFileProvider.createTempFile("source_", "_" + filename);
+ request.setAttribute(SOURCE_FILE, file);
+ save(multipartFile, file);
+ LogEntry.setSource(filename, size);
+ return file;
+ }
+
+ @SuppressWarnings("ResultOfMethodCallIgnored")
+ public static void deleteFile(HttpServletRequest request, String attributeName)
+ {
+ File file = (File) request.getAttribute(attributeName);
+ if (file != null)
+ {
+ file.delete();
+ }
+ }
+
+ public static ResponseEntity createAttachment(String targetFilename, File
+ targetFile)
+ {
+ Resource targetResource = load(targetFile);
+ targetFilename = UriUtils.encodePath(getFilename(targetFilename), "UTF-8");
+ return ResponseEntity.ok().header(CONTENT_DISPOSITION,
+ "attachment; filename*= UTF-8''" + targetFilename).body(targetResource);
+ }
+
+ /**
+ * TempFileProvider - Duplicated and adapted from alfresco-core.
+ */
+ public static class TempFileProvider
+ {
+ public static File createTempFile(final String prefix, final String suffix)
+ {
+ final File directory = getTempDir();
+ try
+ {
+ return File.createTempFile(prefix, suffix, directory);
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(
+ "Failed to created temp file: \n prefix: " + prefix +
+ "\n suffix: " + suffix + "\n directory: " + directory, e);
+ }
+ }
+
+ private static File getTempDir()
+ {
+ final String dirName = "Alfresco";
+ final String systemTempDirPath = System.getProperty("java.io.tmpdir");
+ if (systemTempDirPath == null)
+ {
+ throw new RuntimeException("System property not available: java.io.tmpdir");
+ }
+
+ final File systemTempDir = new File(systemTempDirPath);
+ final File tempDir = new File(systemTempDir, dirName);
+ if (!tempDir.exists() && !tempDir.mkdirs() && !tempDir.exists())
+ {
+ throw new RuntimeException("Failed to create temp directory: " + tempDir);
+ }
+
+ return tempDir;
+ }
+ }
+}
diff --git a/t-engine-base/src/main/java/org/alfresco/transform/base/logging/LogEntry.java b/t-engine-base/src/main/java/org/alfresco/transform/base/logging/LogEntry.java
new file mode 100644
index 00000000..af88f0bf
--- /dev/null
+++ b/t-engine-base/src/main/java/org/alfresco/transform/base/logging/LogEntry.java
@@ -0,0 +1,285 @@
+/*
+ * #%L
+ * Alfresco Transform Core
+ * %%
+ * Copyright (C) 2005 - 2022 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.transform.base.logging;
+
+import static java.lang.Math.max;
+import static org.springframework.http.HttpStatus.OK;
+
+import java.text.SimpleDateFormat;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Deque;
+import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Provides setter and getter methods to allow the current Thread to set various log properties and for these
+ * values to be retrieved. The {@link #complete()} method should be called at the end of a request to flush the
+ * current entry to an internal log Collection of the latest entries. The {@link #getLog()} method is used to obtain
+ * access to this collection.
+ */
+public final class LogEntry
+{
+ private static final Logger logger = LoggerFactory.getLogger(LogEntry.class);
+ // TODO allow ProbeTestTransform to find out if there are any transforms running longer than the max time.
+
+ private static final AtomicInteger count = new AtomicInteger(0);
+ private static final Deque log = new ConcurrentLinkedDeque<>();
+ private static final int MAX_LOG_SIZE = 10;
+ private static final SimpleDateFormat HH_MM_SS = new SimpleDateFormat("HH:mm:ss");
+
+ private static final ThreadLocal currentLogEntry = ThreadLocal.withInitial(() -> {
+ LogEntry logEntry = new LogEntry();
+ if (log.size() >= MAX_LOG_SIZE)
+ {
+ log.removeLast();
+ }
+ log.addFirst(logEntry);
+ return logEntry;
+ });
+
+ private final int id = count.incrementAndGet();
+ private final long start = System.currentTimeMillis();
+ private int statusCode;
+
+ private long durationStreamIn;
+ private long durationTransform = -1;
+ private long durationStreamOut = -1;
+ private String source;
+ private long sourceSize;
+ private String target;
+ private long targetSize = -1;
+ private String options;
+ private String message;
+
+ @Override
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
+ append(sb, Integer.toString(getId()));
+ append(sb, HH_MM_SS.format(getDate()));
+ append(sb, Integer.toString(getStatusCode()));
+ append(sb, getDuration());
+ append(sb, getSource());
+ append(sb, getSourceSize());
+ append(sb, getTarget());
+ append(sb, getTargetSize());
+ append(sb, getOptions());
+ sb.append(getMessage());
+ return sb.toString();
+ }
+
+ private void append(StringBuilder sb, String value)
+ {
+ if (value != null && !value.isEmpty() && !"0bytes".equals(value))
+ {
+ sb.append(value);
+ sb.append(' ');
+ }
+ }
+
+ public static Collection getLog()
+ {
+ return log;
+ }
+
+ public static void start()
+ {
+ currentLogEntry.get();
+ }
+
+ public static void setSource(String source, long sourceSize)
+ {
+ LogEntry logEntry = currentLogEntry.get();
+ logEntry.source = getExtension(source);
+ logEntry.sourceSize = sourceSize;
+ logEntry.durationStreamIn = System.currentTimeMillis() - logEntry.start;
+ }
+
+ public static void setTarget(String target)
+ {
+ currentLogEntry.get().target = getExtension(target);
+ }
+
+ private static String getExtension(String filename)
+ {
+ int i = filename.lastIndexOf('.');
+ if (i != -1)
+ {
+ filename = filename.substring(i + 1);
+ }
+ return filename;
+ }
+
+ public static void setTargetSize(long targetSize)
+ {
+ currentLogEntry.get().targetSize = targetSize;
+ }
+
+ public static void setOptions(String options)
+ {
+ currentLogEntry.get().options = options;
+ }
+
+ public static long setStatusCodeAndMessage(int statusCode, String message)
+ {
+ LogEntry logEntry = currentLogEntry.get();
+ logEntry.statusCode = statusCode;
+ logEntry.message = message;
+ logEntry.durationTransform = System.currentTimeMillis() - logEntry.start - logEntry.durationStreamIn;
+
+ return logEntry.durationTransform;
+ }
+
+ public static void complete()
+ {
+ LogEntry logEntry = currentLogEntry.get();
+ if (logEntry.statusCode == OK.value())
+ {
+ logEntry.durationStreamOut = System.currentTimeMillis() - logEntry.start -
+ logEntry.durationStreamIn - max(logEntry.durationTransform, 0);
+ }
+ currentLogEntry.remove();
+
+ if (logger.isDebugEnabled())
+ {
+ logger.debug(logEntry.toString());
+ }
+ }
+
+ public int getId()
+ {
+ return id;
+ }
+
+ public Date getDate()
+ {
+ return new Date(start);
+ }
+
+ public int getStatusCode()
+ {
+ return statusCode;
+ }
+
+ public String getDuration()
+ {
+ long duration = durationStreamIn + max(durationTransform, 0) + max(durationStreamOut, 0);
+ return duration <= 5
+ ? ""
+ : time(duration) +
+ " (" +
+ (time(durationStreamIn) + ' ' +
+ time(durationTransform) + ' ' +
+ time(durationStreamOut)).trim() +
+ ")";
+ }
+
+ public String getSource()
+ {
+ return source;
+ }
+
+ public String getSourceSize()
+ {
+ return size(sourceSize);
+ }
+
+ public String getTarget()
+ {
+ return target;
+ }
+
+ public String getTargetSize()
+ {
+ return size(targetSize);
+ }
+
+ public String getOptions()
+ {
+ return options;
+ }
+
+ public String getMessage()
+ {
+ return message;
+ }
+
+ private String time(long ms)
+ {
+ return ms == -1 ? "" : size(ms, "1ms",
+ new String[]{"ms", "s", "min", "hr"},
+ new long[]{1000, 60 * 1000, 60 * 60 * 1000, Long.MAX_VALUE});
+ }
+
+ private String size(long size)
+ {
+ // TODO fix numeric overflow in TB expression
+ return size == -1 ? "" : size(size, "1 byte",
+ new String[]{"bytes", " KB", " MB", " GB", " TB"},
+ new long[]{1024, 1024 * 1024, 1024 * 1024 * 1024, 1024 * 1024 * 1024 * 1024, Long.MAX_VALUE});
+ }
+
+ private String size(long size, String singleValue, String[] units, long[] dividers)
+ {
+ if (size == 1)
+ {
+ return singleValue;
+ }
+ long divider = 1;
+ for (int i = 0; i < units.length - 1; i++)
+ {
+ long nextDivider = dividers[i];
+ if (size < nextDivider)
+ {
+ return unitFormat(size, divider, units[i]);
+ }
+ divider = nextDivider;
+ }
+ return unitFormat(size, divider, units[units.length - 1]);
+ }
+
+ private String unitFormat(long size, long divider, String unit)
+ {
+ size = size * 10 / divider;
+ int decimalPoint = (int) size % 10;
+
+ StringBuilder sb = new StringBuilder();
+ sb.append(size / 10);
+ if (decimalPoint != 0)
+ {
+ sb.append(".");
+ sb.append(decimalPoint);
+ }
+ sb.append(unit);
+
+ return sb.toString();
+ }
+}
diff --git a/t-engine-base/src/main/java/org/alfresco/transform/base/logging/StandardMessages.java b/t-engine-base/src/main/java/org/alfresco/transform/base/logging/StandardMessages.java
new file mode 100644
index 00000000..1eb6bc79
--- /dev/null
+++ b/t-engine-base/src/main/java/org/alfresco/transform/base/logging/StandardMessages.java
@@ -0,0 +1,35 @@
+/*
+ * #%L
+ * Alfresco Transform Core
+ * %%
+ * Copyright (C) 2005 - 2022 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.transform.base.logging;
+
+public interface StandardMessages
+{
+ String LICENCE =
+ "If the Alfresco software was purchased under a paid Alfresco license, the terms of the paid license agreement \n" +
+ "will prevail. Otherwise, the software is provided under terms of the GNU LGPL v3 license. \n" +
+ "See the license at http://www.gnu.org/licenses/lgpl-3.0.txt. or in /LICENSE.txt \n\n";
+}
diff --git a/t-engine-base/src/main/java/org/alfresco/transform/base/messaging/MessagingConfig.java b/t-engine-base/src/main/java/org/alfresco/transform/base/messaging/MessagingConfig.java
new file mode 100644
index 00000000..4f386e1f
--- /dev/null
+++ b/t-engine-base/src/main/java/org/alfresco/transform/base/messaging/MessagingConfig.java
@@ -0,0 +1,108 @@
+/*
+ * #%L
+ * Alfresco Transform Core
+ * %%
+ * Copyright (C) 2005 - 2022 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.transform.base.messaging;
+
+import javax.jms.ConnectionFactory;
+import javax.jms.Queue;
+
+import org.alfresco.transform.messages.TransformRequestValidator;
+import org.apache.activemq.command.ActiveMQQueue;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.jms.annotation.JmsListenerConfigurer;
+import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
+import org.springframework.jms.config.JmsListenerEndpointRegistrar;
+import org.springframework.jms.connection.JmsTransactionManager;
+import org.springframework.lang.NonNull;
+import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory;
+import org.springframework.transaction.PlatformTransactionManager;
+
+/**
+ * JMS and messaging configuration for the T-Engines. Contains the basic config in order to have the
+ * T-Engine able to read from queues and send a reply back.
+ *
+ * @author Lucian Tuca
+ * created on 18/12/2018
+ */
+@Configuration
+@ConditionalOnProperty(name = "activemq.url")
+public class MessagingConfig implements JmsListenerConfigurer
+{
+ private static final Logger logger = LoggerFactory.getLogger(MessagingConfig.class);
+
+ @Override
+ public void configureJmsListeners(@NonNull JmsListenerEndpointRegistrar registrar)
+ {
+ registrar.setMessageHandlerMethodFactory(methodFactory());
+ }
+
+ @Bean
+ @ConditionalOnProperty(name = "activemq.url")
+ public DefaultMessageHandlerMethodFactory methodFactory()
+ {
+ DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory();
+ factory.setValidator(new TransformRequestValidator());
+ return factory;
+ }
+
+ @Bean
+ @ConditionalOnProperty(name = "activemq.url")
+ public DefaultJmsListenerContainerFactory jmsListenerContainerFactory(
+ final ConnectionFactory connectionFactory,
+ final TransformMessageConverter transformMessageConverter)
+ {
+ final DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
+ factory.setConnectionFactory(connectionFactory);
+ factory.setMessageConverter(transformMessageConverter);
+ factory.setErrorHandler(t -> logger.error("JMS error: " + t.getMessage(), t));
+ factory.setTransactionManager(transactionManager(connectionFactory));
+ return factory;
+ }
+
+ @Bean
+ @ConditionalOnProperty(name = "activemq.url")
+ public PlatformTransactionManager transactionManager(final ConnectionFactory connectionFactory)
+ {
+ final JmsTransactionManager transactionManager = new JmsTransactionManager();
+ transactionManager.setConnectionFactory(connectionFactory);
+ return transactionManager;
+ }
+
+ @Bean
+ @ConditionalOnProperty(name = "activemq.url")
+ public Queue engineRequestQueue(
+ @Value("${queue.engineRequestQueue}") String engineRequestQueueValue)
+ {
+ return new ActiveMQQueue(engineRequestQueueValue);
+ }
+}
+
+
diff --git a/t-engine-base/src/main/java/org/alfresco/transform/base/messaging/MessagingInfo.java b/t-engine-base/src/main/java/org/alfresco/transform/base/messaging/MessagingInfo.java
new file mode 100644
index 00000000..eb353604
--- /dev/null
+++ b/t-engine-base/src/main/java/org/alfresco/transform/base/messaging/MessagingInfo.java
@@ -0,0 +1,67 @@
+/*
+ * #%L
+ * Alfresco Transform Core
+ * %%
+ * Copyright (C) 2005 - 2022 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.transform.base.messaging;
+
+import javax.annotation.PostConstruct;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Prints JMS status information at application startup.
+ *
+ * @author Cezar Leahu
+ */
+@Configuration
+public class MessagingInfo
+{
+ private static final Logger logger = LoggerFactory.getLogger(MessagingInfo.class);
+
+ @Value("${activemq.url:}")
+ private String activemqUrl;
+
+ @PostConstruct
+ public void init()
+ {
+ // For backwards-compatibility, we continue to rely on setting ACTIVEMQ_URL environment variable (see application.yaml)
+ // The MessagingConfig class uses on ConditionalOnProperty (ie. activemq.url is set and not false)
+
+ // Note: as per application.yaml the broker url is appended with "?jms.watchTopicAdvisories=false". If this needs to be fully
+ // overridden then it would require explicitly setting both "spring.activemq.broker-url" *and* "activemq.url" (latter to non-false value).
+
+ if ((activemqUrl != null) && (! activemqUrl.equals("false")))
+ {
+ logger.info("JMS client is ENABLED - ACTIVEMQ_URL ='{}'", activemqUrl);
+ }
+ else
+ {
+ logger.info("JMS client is DISABLED - ACTIVEMQ_URL is not set");
+ }
+ }
+}
diff --git a/t-engine-base/src/main/java/org/alfresco/transform/base/messaging/TransformMessageConverter.java b/t-engine-base/src/main/java/org/alfresco/transform/base/messaging/TransformMessageConverter.java
new file mode 100644
index 00000000..40f4113d
--- /dev/null
+++ b/t-engine-base/src/main/java/org/alfresco/transform/base/messaging/TransformMessageConverter.java
@@ -0,0 +1,102 @@
+/*
+ * #%L
+ * Alfresco Transform Core
+ * %%
+ * Copyright (C) 2005 - 2022 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+
+package org.alfresco.transform.base.messaging;
+
+import javax.jms.JMSException;
+import javax.jms.Message;
+import javax.jms.Session;
+
+import org.alfresco.transform.client.model.TransformReply;
+import org.alfresco.transform.client.model.TransformRequest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.jms.support.converter.MappingJackson2MessageConverter;
+import org.springframework.jms.support.converter.MessageConversionException;
+import org.springframework.jms.support.converter.MessageConverter;
+import org.springframework.jms.support.converter.MessageType;
+import org.springframework.lang.NonNull;
+import org.springframework.stereotype.Service;
+
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.type.TypeFactory;
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * TODO: Duplicated from the Router
+ * Custom wrapper over MappingJackson2MessageConverter for T-Request/T-Reply objects.
+ *
+ * @author Cezar Leahu
+ */
+@Service
+public class TransformMessageConverter implements MessageConverter
+{
+ private static final Logger logger = LoggerFactory.getLogger(TransformMessageConverter.class);
+
+ private static final MappingJackson2MessageConverter converter;
+ private static final JavaType TRANSFORM_REQUEST_TYPE =
+ TypeFactory.defaultInstance().constructType(TransformRequest.class);
+
+ static
+ {
+ converter = new MappingJackson2MessageConverter()
+ {
+ @Override
+ @NonNull
+ protected JavaType getJavaTypeForMessage(final Message message) throws JMSException
+ {
+ if (message.getStringProperty("_type") == null)
+ {
+ return TRANSFORM_REQUEST_TYPE;
+ }
+ return super.getJavaTypeForMessage(message);
+ }
+ };
+ converter.setTargetType(MessageType.BYTES);
+ converter.setTypeIdPropertyName("_type");
+ converter.setTypeIdMappings(ImmutableMap.of(
+ TransformRequest.class.getName(), TransformRequest.class,
+ TransformReply.class.getName(), TransformReply.class)
+ );
+ }
+
+ @Override
+ @NonNull
+ public Message toMessage(
+ @NonNull final Object object,
+ @NonNull final Session session) throws JMSException, MessageConversionException
+ {
+ return converter.toMessage(object, session);
+ }
+
+ @Override
+ @NonNull
+ public Object fromMessage(@NonNull final Message message) throws JMSException
+ {
+ return converter.fromMessage(message);
+ }
+}
diff --git a/t-engine-base/src/main/java/org/alfresco/transform/base/messaging/TransformReplySender.java b/t-engine-base/src/main/java/org/alfresco/transform/base/messaging/TransformReplySender.java
new file mode 100644
index 00000000..d14c1660
--- /dev/null
+++ b/t-engine-base/src/main/java/org/alfresco/transform/base/messaging/TransformReplySender.java
@@ -0,0 +1,77 @@
+/*
+ * #%L
+ * Alfresco Transform Core
+ * %%
+ * Copyright (C) 2005 - 2022 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.transform.base.messaging;
+
+import javax.jms.Destination;
+
+import org.alfresco.transform.client.model.TransformReply;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jms.core.JmsTemplate;
+import org.springframework.stereotype.Component;
+
+/**
+ * TODO: Duplicated from the Router
+ * TransformReplySender Bean
+ *
+ * JMS message sender/publisher
+ *
+ * @author Cezar Leahu
+ */
+@Component
+public class TransformReplySender
+{
+ private static final Logger logger = LoggerFactory.getLogger(TransformReplySender.class);
+
+ @Autowired
+ private JmsTemplate jmsTemplate;
+
+ public void send(final Destination destination, final TransformReply reply)
+ {
+ send(destination, reply, reply.getRequestId());
+ }
+
+ public void send(final Destination destination, final TransformReply reply,
+ final String correlationId)
+ {
+ try
+ {
+ //jmsTemplate.setSessionTransacted(true); // do we need this?
+ jmsTemplate.convertAndSend(destination, reply, m -> {
+ m.setJMSCorrelationID(correlationId);
+ return m;
+ });
+ logger.trace("Sent: {} - with correlation ID {}", reply, correlationId);
+ }
+ catch (Exception e)
+ {
+ logger.error(
+ "Failed to send T-Reply " + reply + " - for correlation ID " + correlationId, e);
+ }
+ }
+}
diff --git a/t-engine-base/src/main/java/org/alfresco/transform/base/metadataExtractors/AbstractMetadataExtractor.java b/t-engine-base/src/main/java/org/alfresco/transform/base/metadataExtractors/AbstractMetadataExtractor.java
new file mode 100644
index 00000000..c1bb9817
--- /dev/null
+++ b/t-engine-base/src/main/java/org/alfresco/transform/base/metadataExtractors/AbstractMetadataExtractor.java
@@ -0,0 +1,599 @@
+/*
+ * #%L
+ * Alfresco Transform Core
+ * %%
+ * Copyright (C) 2005-2022 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * -
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ * -
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * -
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ * -
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+package org.alfresco.transform.base.metadataExtractors;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.alfresco.transform.common.TransformException;
+import org.slf4j.Logger;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Serializable;
+import java.lang.reflect.Array;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.TreeMap;
+
+/**
+ * Helper methods for metadata extract and embed.
+ *
+ * Much of the code is based on AbstractMappingMetadataExtracter from the
+ * content repository. The code has been simplified to only set up mapping one way.
+ *
+ * If a transform specifies that it can convert from {@code ""} to {@code "alfresco-metadata-extract"}
+ * (specified in the {@code engine_config.json}), it is indicating that it can extract metadata from {@code }.
+ *
+ * The transform results in a Map of extracted properties encoded as json being returned to the content repository.
+ *
+ *
The content repository will use a transform in preference to any metadata extractors it might have defined
+ * locally for the same MIMETYPE.
+ *
The T-Engine's Controller class will call a method in a class that extends {@link AbstractMetadataExtractor}
+ * based on the source and target mediatypes in the normal way.
+ *
The method extracts ALL available metadata is extracted from the document and then calls
+ * {@link #mapMetadataAndWrite(File, Map, Map)}.
+ *
Selected values from the available metadata are mapped into content repository property names and values,
+ * depending on what is defined in a {@code "_metadata_extract.properties"} file.
+ *
The selected values are set back to the content repository as a JSON representation of a Map, where the values
+ * are applied to the source node.
+ *
+ * To support the same functionality as metadata extractors configured inside the content repository,
+ * extra key value pairs may be returned from {@link #extractMetadata}. These are:
+ *
+ *
{@code "sys:overwritePolicy"} which can specify the
+ * {@code org.alfresco.repo.content.metadata.MetadataExtracter.OverwritePolicy} name. Defaults to "PRAGMATIC".
+ *
{@code "sys:enableStringTagging"} if {@code "true"} finds or creates tags for each string mapped to
+ * {@code cm:taggable}. Defaults to {@code "false"} to ignore mapping strings to tags.
+ *
{@code "sys:carryAspectProperties"}
+ *
{@code "sys:stringTaggingSeparators"}
+ *
+ *
+ * If a transform specifies that it can convert from {@code ""} to {@code "alfresco-metadata-embed"}, it is
+ * indicating that it can embed metadata in {@code }.
+ *
+ * The transform results in a new version of supplied source file that contains the metadata supplied in the transform
+ * options.
+ *
+ * @author Jesper Steen Møller
+ * @author Derek Hulley
+ * @author adavis
+ */
+public abstract class AbstractMetadataExtractor
+{
+ private static final String EXTRACT = "extract";
+ private static final String EMBED = "embed";
+ private static final String METADATA = "metadata";
+ private static final String EXTRACT_MAPPING = "extractMapping";
+
+ private static final String NAMESPACE_PROPERTY_PREFIX = "namespace.prefix.";
+ private static final char NAMESPACE_PREFIX = ':';
+ private static final char NAMESPACE_BEGIN = '{';
+ private static final char NAMESPACE_END = '}';
+
+ private static final List SYS_PROPERTIES = Arrays.asList(
+ "sys:overwritePolicy",
+ "sys:enableStringTagging",
+ "sys:carryAspectProperties",
+ "sys:stringTaggingSeparators");
+
+ private static final ObjectMapper jsonObjectMapper = new ObjectMapper();
+
+ protected final Logger logger;
+ private Map> defaultExtractMapping;
+ private ThreadLocal