mirror of
https://github.com/Alfresco/alfresco-transform-core.git
synced 2025-10-08 14:51:18 +00:00
HXENG-64 refactor ATS (#657)
Refactor to clean up packages in the t-model and to introduce a simpler to implement t-engine base. The new t-engines (tika, imagemagick, libreoffice, pdfrenderer, misc, aio, aspose) and t-router may be used in combination with older components as the API between the content Repo and between components has not changed. As far as possible the same artifacts are created (the -boot projects no longer exist). They may be used with older ACS repo versions. The main changes to look for are: * The introduction of TransformEngine and CustomTransformer interfaces to be implemented. * The removal in t-engines and t-router of the Controller, Application, test template page, Controller tests and application config, as this is all now done by the t-engine base package. * The t-router now extends the t-engine base, which also reduced the amount of duplicate code. * The t-engine base provides the test page, which includes drop downs of known transform options. The t-router is able to use pipeline and failover transformers. This was not possible to do previously as the router had no test UI. * Resources including licenses are automatically included in the all-in-one t-engine, from the individual t-engines. They just need to be added as dependencies in the pom. * The ugly code in the all-in-one t-engine and misc t-engine to pick transformers has gone, as they are now just selected by the transformRegistry. * The way t-engines respond to http or message queue transform requests has been combined (eliminates the similar but different code that existed before). * The t-engine base now uses InputStream and OutputStream rather than Files by default. As a result it will be simpler to avoid writing content to a temporary location. * A number of the Tika and Misc CustomTransforms no longer use Files. * The original t-engine base still exists so customers can continue to create custom t-engines the way they have done previously. the project has just been moved into a folder called deprecated. * The folder structure has changed. The long "alfresco-transform-..." names have given way to shorter easier to read and type names. * The t-engine project structure now has a single project rather than two. * The previous config values still exist, but there are now a new set for config values for in files with names that don't misleadingly imply they only contain pipeline of routing information. * The concept of 'routing' has much less emphasis in class names as the code just uses the transformRegistry. * TransformerConfig may now be read as json or yaml. The restrictions about what could be specified in yaml has gone. * T-engines and t-router may use transform config from files. Previously it was just the t-router. * The POC code to do with graphs of possible routes has been removed. * All master branch changes have been merged in. * The concept of a single transform request which results in multiple responses (e.g. images from a video) has been added to the core processing of requests in the t-engine base. * Many SonarCloud linter fixes.
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base;
|
||||
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.task.SimpleAsyncTaskExecutor;
|
||||
import org.springframework.core.task.TaskExecutor;
|
||||
import org.springframework.retry.annotation.EnableRetry;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableAsync
|
||||
@EnableScheduling
|
||||
@EnableRetry
|
||||
public class Application
|
||||
{
|
||||
@Value("${container.name}")
|
||||
private String containerName;
|
||||
|
||||
@Bean
|
||||
public TaskExecutor taskExecutor()
|
||||
{
|
||||
return new SimpleAsyncTaskExecutor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
MeterRegistryCustomizer<MeterRegistry> metricsCommonTags()
|
||||
{
|
||||
return registry -> registry.config().commonTags("containerName", containerName);
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base;
|
||||
|
||||
import org.alfresco.transform.config.TransformConfig;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Interface to be implemented by transform specific code. The {@code transformerName} should match the transformerName
|
||||
* in the {@link TransformConfig} returned by the {@link TransformEngine}. So that it is automatically picked up, it
|
||||
* must exist in a package under {@code org.alfresco.transform} and have the Spring {@code @Component} annotation.
|
||||
*
|
||||
* Implementations may also use the {@link TransformManager} if they wish to interact with the base t-engine.
|
||||
*/
|
||||
public interface CustomTransformer
|
||||
{
|
||||
String getTransformerName();
|
||||
|
||||
void transform(String sourceMimetype, InputStream inputStream,
|
||||
String targetMimetype, OutputStream outputStream,
|
||||
Map<String, String> transformOptions, TransformManager transformManager) throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base;
|
||||
|
||||
import org.alfresco.transform.base.logging.LogEntry;
|
||||
import org.alfresco.transform.base.probes.ProbeTransform;
|
||||
import org.alfresco.transform.base.registry.TransformRegistry;
|
||||
import org.alfresco.transform.base.transform.TransformHandler;
|
||||
import org.alfresco.transform.client.model.TransformReply;
|
||||
import org.alfresco.transform.client.model.TransformRequest;
|
||||
import org.alfresco.transform.exceptions.TransformException;
|
||||
import org.alfresco.transform.config.TransformConfig;
|
||||
import org.alfresco.transform.registry.TransformServiceRegistry;
|
||||
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.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
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.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.jms.Destination;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static java.text.MessageFormat.format;
|
||||
import static org.alfresco.transform.base.html.OptionsHelper.getOptionNames;
|
||||
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_ERROR;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_LIVE;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_LOG;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_READY;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_ROOT;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_TEST;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_TRANSFORM;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_TRANSFORM_CONFIG;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_VERSION;
|
||||
import static org.alfresco.transform.common.RequestParamMap.FILE;
|
||||
import static org.alfresco.transform.common.RequestParamMap.SOURCE_MIMETYPE;
|
||||
import static org.alfresco.transform.common.RequestParamMap.TARGET_MIMETYPE;
|
||||
import static org.alfresco.transform.config.CoreVersionDecorator.setOrClearCoreVersion;
|
||||
import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Provides the main endpoints into the t-engine.
|
||||
*/
|
||||
@Controller
|
||||
public class TransformController
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(TransformController.class);
|
||||
|
||||
private static final String MODEL_TITLE = "title";
|
||||
private static final String MODEL_PROXY_PATH_PREFIX = "proxyPathPrefix";
|
||||
private static final String MODEL_MESSAGE = "message";
|
||||
|
||||
@Autowired(required = false)
|
||||
private List<TransformEngine> transformEngines;
|
||||
@Autowired
|
||||
private TransformServiceRegistry transformRegistry;
|
||||
@Autowired
|
||||
TransformHandler transformHandler;
|
||||
@Autowired
|
||||
private String coreVersion;
|
||||
@Value("${container.behind-ingres}")
|
||||
private boolean behindIngres;
|
||||
|
||||
TransformEngine transformEngine;
|
||||
|
||||
@PostConstruct
|
||||
private void initTransformEngine()
|
||||
{
|
||||
if (transformEngines != null)
|
||||
{
|
||||
transformEngine = getTransformEngine();
|
||||
logger.info("TransformEngine: {}", transformEngine.getTransformEngineName());
|
||||
transformEngines.stream()
|
||||
.filter(transformEngineFromStream -> transformEngineFromStream != transformEngine)
|
||||
.sorted(Comparator.comparing(TransformEngine::getTransformEngineName))
|
||||
.map(sortedTransformEngine -> " "+sortedTransformEngine.getTransformEngineName()).forEach(logger::info);
|
||||
}
|
||||
}
|
||||
|
||||
private TransformEngine getTransformEngine()
|
||||
{
|
||||
// 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).
|
||||
return transformEngines.stream()
|
||||
.filter(transformEngineFromStream -> transformEngineFromStream.getTransformConfig() == null)
|
||||
.findFirst()
|
||||
.orElse(transformEngines.get(0));
|
||||
}
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void startup()
|
||||
{
|
||||
logger.info("--------------------------------------------------------------------------------------------------------------------------------------------------------------");
|
||||
if (transformEngines != null)
|
||||
{
|
||||
logSplitMessage(transformEngine.getStartupMessage());
|
||||
}
|
||||
logger.info("--------------------------------------------------------------------------------------------------------------------------------------------------------------");
|
||||
logger.info("Starting application components... Done");
|
||||
}
|
||||
|
||||
private void logSplitMessage(String message)
|
||||
{
|
||||
Arrays.stream(message.split("\\n")).forEach(logger::info);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a string that may be used in client debug.
|
||||
*/
|
||||
@RequestMapping(ENDPOINT_VERSION)
|
||||
@ResponseBody
|
||||
public String version()
|
||||
{
|
||||
return getSimpleTransformEngineName() + ' ' + coreVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test UI page to perform a transform.
|
||||
*/
|
||||
@GetMapping(ENDPOINT_ROOT)
|
||||
public String test(Model model)
|
||||
{
|
||||
model.addAttribute(MODEL_TITLE, getSimpleTransformEngineName() + " Test Page");
|
||||
model.addAttribute(MODEL_PROXY_PATH_PREFIX, getPathPrefix());
|
||||
TransformConfig transformConfig = ((TransformRegistry) transformRegistry).getTransformConfig();
|
||||
transformConfig = setOrClearCoreVersion(transformConfig, 0);
|
||||
model.addAttribute("transformOptions", getOptionNames(transformConfig.getTransformOptions()));
|
||||
return "test"; // display test.html
|
||||
}
|
||||
|
||||
/**
|
||||
* Test UI error page.
|
||||
*/
|
||||
@GetMapping(ENDPOINT_ERROR)
|
||||
public String error(Model model)
|
||||
{
|
||||
model.addAttribute(MODEL_TITLE, getSimpleTransformEngineName() + " Error Page");
|
||||
model.addAttribute(MODEL_PROXY_PATH_PREFIX, getPathPrefix());
|
||||
return "error"; // display error.html
|
||||
}
|
||||
|
||||
/**
|
||||
* Test UI log page.
|
||||
*/
|
||||
@GetMapping(ENDPOINT_LOG)
|
||||
String log(Model model)
|
||||
{
|
||||
model.addAttribute(MODEL_TITLE, getSimpleTransformEngineName() + " Log Entries");
|
||||
model.addAttribute(MODEL_PROXY_PATH_PREFIX, getPathPrefix());
|
||||
Collection<LogEntry> log = LogEntry.getLog();
|
||||
if (!log.isEmpty())
|
||||
{
|
||||
model.addAttribute("log", log);
|
||||
}
|
||||
return "log"; // display log.html
|
||||
}
|
||||
|
||||
private Object getPathPrefix()
|
||||
{
|
||||
String pathPrefix = "";
|
||||
if (behindIngres)
|
||||
{
|
||||
pathPrefix = "/" + getSimpleTransformEngineName().toLowerCase();
|
||||
}
|
||||
return pathPrefix;
|
||||
}
|
||||
|
||||
private String getSimpleTransformEngineName()
|
||||
{
|
||||
return transformEngine.getTransformEngineName().replaceFirst("^\\d+ ", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Kubernetes readiness probe.
|
||||
*/
|
||||
@GetMapping(ENDPOINT_READY)
|
||||
@ResponseBody
|
||||
public String ready(HttpServletRequest request)
|
||||
{
|
||||
// An alternative without transforms might be: ((TransformRegistry)transformRegistry).isReadyForTransformRequests();
|
||||
return getProbeTransform().doTransformOrNothing(false, transformHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kubernetes liveness probe.
|
||||
*/
|
||||
@GetMapping(ENDPOINT_LIVE)
|
||||
@ResponseBody
|
||||
public String live(HttpServletRequest request)
|
||||
{
|
||||
return getProbeTransform().doTransformOrNothing(true, transformHandler);
|
||||
}
|
||||
|
||||
public ProbeTransform getProbeTransform()
|
||||
{
|
||||
return transformEngine.getProbeTransform();
|
||||
}
|
||||
|
||||
@GetMapping(value = ENDPOINT_TRANSFORM_CONFIG)
|
||||
public ResponseEntity<TransformConfig> transformConfig(
|
||||
@RequestParam(value = CONFIG_VERSION, defaultValue = CONFIG_VERSION_DEFAULT) int configVersion)
|
||||
{
|
||||
logger.info("GET Transform Config version: " + configVersion);
|
||||
TransformConfig transformConfig = ((TransformRegistry) transformRegistry).getTransformConfig();
|
||||
transformConfig = setOrClearCoreVersion(transformConfig, configVersion);
|
||||
return new ResponseEntity<>(transformConfig, OK);
|
||||
}
|
||||
|
||||
// Only used for testing, but could be used in place of the /transform endpoint used by Alfresco Repository's
|
||||
// 'Local Transforms'. In production, TransformRequests are processed is via a message queue.
|
||||
@PostMapping(value = ENDPOINT_TRANSFORM, produces = APPLICATION_JSON_VALUE)
|
||||
@ResponseBody
|
||||
public ResponseEntity<TransformReply> transform(@RequestBody TransformRequest request,
|
||||
@RequestParam(value = "timeout", required = false) Long timeout,
|
||||
@RequestParam(value = "replyToQueue", required = false) Destination replyToQueue)
|
||||
{
|
||||
TransformReply reply = transformHandler.handleMessageRequest(request, timeout, replyToQueue, getProbeTransform());
|
||||
return new ResponseEntity<>(reply, HttpStatus.valueOf(reply.getStatus()));
|
||||
}
|
||||
|
||||
// Used by Alfresco Repository's 'Local Transforms'. Uploads the content and downloads the result.
|
||||
@PostMapping(value = ENDPOINT_TRANSFORM, consumes = MULTIPART_FORM_DATA_VALUE)
|
||||
public ResponseEntity<Resource> transform(HttpServletRequest request,
|
||||
@RequestParam(value = FILE, required = false) MultipartFile sourceMultipartFile,
|
||||
@RequestParam(value = SOURCE_MIMETYPE) String sourceMimetype,
|
||||
@RequestParam(value = TARGET_MIMETYPE) String targetMimetype,
|
||||
@RequestParam Map<String, String> requestParameters)
|
||||
{
|
||||
return transformHandler.handleHttpRequest(request, sourceMultipartFile, sourceMimetype,
|
||||
targetMimetype, requestParameters, getProbeTransform());
|
||||
}
|
||||
|
||||
// Used the t-engine's simple html test UI.
|
||||
@PostMapping(value = ENDPOINT_TEST, consumes = MULTIPART_FORM_DATA_VALUE)
|
||||
public ResponseEntity<Resource> testTransform(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<String, String> origRequestParameters)
|
||||
{
|
||||
// Remaps request parameters from test.html and hands them off to the normal transform endpoint.
|
||||
// There are name<i> and value<i> parameters which allow dynamic names and values to be used.
|
||||
Map<String, String> requestParameters = new HashMap<>();
|
||||
sourceMimetype = overrideMimetypeFromExtension(origRequestParameters, SOURCE_MIMETYPE, sourceMimetype);
|
||||
targetMimetype = overrideMimetypeFromExtension(origRequestParameters, TARGET_MIMETYPE, targetMimetype);
|
||||
origRequestParameters.forEach((name, value) ->
|
||||
{
|
||||
if (!name.startsWith("value"))
|
||||
{
|
||||
if (name.startsWith("name"))
|
||||
{
|
||||
String suffix = name.substring("name".length());
|
||||
name = value;
|
||||
value = origRequestParameters.get("value" + suffix);
|
||||
}
|
||||
if (name != null && !name.isBlank() && value != null && !value.isBlank())
|
||||
{
|
||||
requestParameters.put(name, value);
|
||||
}
|
||||
}
|
||||
});
|
||||
return transform(request, sourceMultipartFile, sourceMimetype, targetMimetype, requestParameters);
|
||||
}
|
||||
|
||||
private String overrideMimetypeFromExtension(Map<String, String> origRequestParameters, String name, String value)
|
||||
{
|
||||
String override = origRequestParameters.remove("_"+ name);
|
||||
if (override != null && !override.isBlank())
|
||||
{
|
||||
value = override;
|
||||
origRequestParameters.put(name, value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@ExceptionHandler(MissingServletRequestParameterException.class)
|
||||
public void handleMissingParams(HttpServletResponse response, MissingServletRequestParameterException e)
|
||||
throws IOException
|
||||
{
|
||||
final String message = format("Request parameter ''{0}'' is missing", e.getParameterName());
|
||||
logger.error(message, e);
|
||||
response.sendError(BAD_REQUEST.value(), message);
|
||||
}
|
||||
|
||||
@ExceptionHandler(TransformException.class)
|
||||
public ModelAndView handleTransformException(HttpServletResponse response, TransformException e)
|
||||
throws IOException
|
||||
{
|
||||
final String message = e.getMessage();
|
||||
logger.error(message);
|
||||
response.sendError(e.getStatus().value(), message);
|
||||
|
||||
ModelAndView mav = new ModelAndView();
|
||||
mav.addObject(MODEL_TITLE, getSimpleTransformEngineName() + " Error Page");
|
||||
mav.addObject(MODEL_PROXY_PATH_PREFIX, getPathPrefix());
|
||||
mav.addObject(MODEL_MESSAGE, message);
|
||||
mav.setViewName("error"); // display error.html
|
||||
return mav;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base;
|
||||
|
||||
import org.alfresco.transform.base.probes.ProbeTransform;
|
||||
import org.alfresco.transform.config.reader.TransformConfigResourceReader;
|
||||
import org.alfresco.transform.config.TransformConfig;
|
||||
|
||||
/**
|
||||
* Interface to be implemented by transform specific code. Provides information about the t-engine as a whole.
|
||||
* Also see {@link CustomTransformer} which provides the code that performs transformation. There may be several
|
||||
* in a single t-engine. So that it is automatically picked up, it must exist in a package under
|
||||
* {@code org.alfresco.transform} and have the Spring {@code @Component} annotation.
|
||||
*/
|
||||
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 using a
|
||||
* {@link TransformConfigResourceReader}. To combine to code from multiple t-engine into a single t-engine
|
||||
* include all the TransformEngines and CustomTransform implementations, plus a wrapper TransformEngine for the
|
||||
* others. The wrapper should return {@code null} from this method.
|
||||
*/
|
||||
TransformConfig getTransformConfig();
|
||||
|
||||
/**
|
||||
* @return a ProbeTransform (will do a quick transform) for k8 liveness and readiness probes.
|
||||
*/
|
||||
ProbeTransform getProbeTransform();
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base;
|
||||
|
||||
import org.alfresco.transform.exceptions.TransformException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Allows {@link CustomTransformer} implementations to interact with the base t-engine.
|
||||
*/
|
||||
public interface TransformManager
|
||||
{
|
||||
/**
|
||||
* @return the id of the request.
|
||||
*/
|
||||
String getRequestId();
|
||||
|
||||
/**
|
||||
* Allows a {@link CustomTransformer} to use a local source {@code File} rather than the supplied {@code InputStream}.
|
||||
* The file will be deleted once the request is completed. To avoid creating extra files, if a File has already
|
||||
* been created by the base t-engine, it is returned.
|
||||
* If possible this method should be avoided as it is better not to leave content on disk.
|
||||
* @throws IllegalStateException if this method has already been called.
|
||||
*/
|
||||
File createSourceFile() throws IllegalStateException;
|
||||
|
||||
/**
|
||||
* Allows a {@link CustomTransformer} to use a local target {@code File} rather than the supplied {@code OutputStream}.
|
||||
* The file will be deleted once the request is completed. To avoid creating extra files, if a File has already
|
||||
* been created by the base t-engine, it is returned.
|
||||
* If possible this method should be avoided as it is better not to leave content on disk.
|
||||
* @throws IllegalStateException if this method has already been called. A call to {@link #respondWithFragment(Integer, boolean)}
|
||||
* allows the method to be called again.
|
||||
*/
|
||||
File createTargetFile() throws IllegalStateException;
|
||||
|
||||
/**
|
||||
* Allows a single transform request to have multiple transform responses. For example, images from a video at
|
||||
* different time offsets or different pages of a document. Following a call to this method a transform response is
|
||||
* made with the data sent to the current {@code OutputStream}. If this method has been called, there will not be
|
||||
* another response when {@link CustomTransformer#transform(String, InputStream, String, OutputStream, Map,
|
||||
* TransformManager)} returns and any data written to the final {@code OutputStream} will be ignored.
|
||||
* @param index returned with the response, so that the fragment may be distinguished from other responses.
|
||||
* Renditions use the index as an offset into elements. A {@code null} value indicates that there
|
||||
* is no more output and any data sent to the current {@code outputStream} will be ignored.
|
||||
* @param finished indicates this is the final fragment. {@code False} indicates that it is expected there will be
|
||||
* more fragments. There need not be a call with this parameter set to {@code true}.
|
||||
* @return a new {@code OutputStream} for the next fragment. A {@code null} will be returned if {@code index} was
|
||||
* {@code null} or {@code finished} was {@code true}.
|
||||
* @throws TransformException if a synchronous (http) request has been made as this only works with requests
|
||||
* on queues, or the first call to this method indicated there was no output, or
|
||||
* another call is made after it has been indicated that there should be no more
|
||||
* fragments.
|
||||
* @throws IOException if there was a problem sending the response.
|
||||
*/
|
||||
// This works because all the state is in the TransformResponse and the t-router will just see each response as
|
||||
// something to either return to the client or pass to the next stage in a pipeline. We might be able to enhance
|
||||
// the logging to include the index. We may also wish to modify the client data or just make the index available
|
||||
// in the message.
|
||||
OutputStream respondWithFragment(Integer index, boolean finished) throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.config;
|
||||
|
||||
import org.alfresco.transform.base.html.TransformInterceptor;
|
||||
import org.alfresco.transform.base.registry.TransformConfigSource;
|
||||
import org.alfresco.transform.common.TransformerDebug;
|
||||
import org.alfresco.transform.messages.TransformRequestValidator;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.FilterType;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_TRANSFORM;
|
||||
import static org.alfresco.transform.config.CoreFunction.standardizeCoreVersion;
|
||||
|
||||
@Configuration
|
||||
@ComponentScan(
|
||||
basePackages = {"org.alfresco.transform"},
|
||||
excludeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*Test.*"))
|
||||
public class WebApplicationConfig implements WebMvcConfigurer
|
||||
{
|
||||
@Value("${transform.core.version}")
|
||||
private String coreVersionString;
|
||||
|
||||
@Value("${container.isTRouter}")
|
||||
private boolean isTRouter;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry)
|
||||
{
|
||||
registry.addInterceptor(new TransformInterceptor())
|
||||
.addPathPatterns(ENDPOINT_TRANSFORM, "/live", "/ready");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate()
|
||||
{
|
||||
return new RestTemplate();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TransformRequestValidator transformRequestValidator()
|
||||
{
|
||||
return new TransformRequestValidator();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TransformerDebug transformerDebug()
|
||||
{
|
||||
return new TransformerDebug().setIsTRouter(isTRouter);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public String coreVersion()
|
||||
{
|
||||
return standardizeCoreVersion(coreVersionString);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public List<TransformConfigSource> transformConfigSources()
|
||||
{
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #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.exceptions.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<String, String> 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, "Transformer exit code was not 0: \n" + result.getStdErr());
|
||||
}
|
||||
|
||||
if (!targetFile.exists() || targetFile.length() == 0)
|
||||
{
|
||||
throw new TransformException(INTERNAL_SERVER_ERROR, "Transformer failed to create an output file");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
* #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
|
||||
{
|
||||
void run(Map<String, String> properties, File targetFile, Long timeout);
|
||||
|
||||
default void run(String options, File sourceFile, File targetFile, Long timeout)
|
||||
{
|
||||
LogEntry.setOptions(options);
|
||||
|
||||
Map<String, String> 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<String, String> properties = new HashMap<>();
|
||||
properties.put("options", options);
|
||||
properties.put("source", sourceFile.getAbsolutePath() + pageRange);
|
||||
properties.put("target", targetFile.getAbsolutePath());
|
||||
|
||||
run(properties, targetFile, timeout);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #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;
|
||||
|
||||
/**
|
||||
* This class is used to tokenize strings used as parameters for {@link RuntimeExec} objects.
|
||||
* Examples of such strings are as follows (ImageMagick-like parameters):
|
||||
* <ul>
|
||||
* <li><tt>-font Helvetica -pointsize 50</tt></li>
|
||||
* <li><tt>-font Helvetica -pointsize 50 -draw "circle 100,100 150,150"</tt></li>
|
||||
* <li><tt>-font Helvetica -pointsize 50 -draw "gravity south fill black text 0,12 'CopyRight'"</tt></li>
|
||||
* </ul>
|
||||
* The first is the simple case which would be parsed into Strings as follows:
|
||||
* <tt>"-font", "Helvetica", "-pointsize", "50"</tt>
|
||||
* <p/>
|
||||
* The second is more complex in that it includes a quoted parameter, which would be parsed as a single String:
|
||||
* <tt>"-font", "Helvetica", "-pointsize", "50", "circle 100,100 150,150"</tt>
|
||||
* Note however that the quotation characters will be stripped from the token.
|
||||
* <p/>
|
||||
* The third shows an example with embedded quotation marks, which would parse to:
|
||||
* <tt>"-font", "Helvetica", "-pointsize", "50", "gravity south fill black text 0,12 'CopyRight'"</tt>
|
||||
* In this case, the embedded quotation marks (which must be different from those surrounding the parameter)
|
||||
* are preserved in the extracted token.
|
||||
* <p/>
|
||||
* The class does not understand escaped quotes such as <tt>p1 p2 "a b c \"hello\" d" p4</tt>
|
||||
*
|
||||
* @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<String> 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.
|
||||
* <p/>
|
||||
* See above for examples.
|
||||
*
|
||||
* @throws NullPointerException if the string to be tokenized was null.
|
||||
*/
|
||||
public List<String> 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<Pair<Integer, Integer>> quotedRegions = new ArrayList<>();
|
||||
|
||||
for (Pair<Integer, Integer> 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<Substring> 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<Substring> getSubstrings(String str,
|
||||
List<Pair<Integer, Integer>> quotedRegionIndices)
|
||||
{
|
||||
List<Substring> result = new ArrayList<>();
|
||||
|
||||
int cursorPosition = 0;
|
||||
for (Pair<Integer, Integer> 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<Integer, Integer> 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<Integer, Integer> 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<String> 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<String> getTokens()
|
||||
{
|
||||
StringTokenizer t = new StringTokenizer(regionString);
|
||||
List<String> 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<String> 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<F, S> 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 + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* #%L
|
||||
* Alfresco Transform Core
|
||||
* %%
|
||||
* Copyright (C) 2005 - 2020 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 <http://www.gnu.org/licenses/>.
|
||||
* #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
|
||||
{
|
||||
void call(File sourceFile, File targetFile, String... args) throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,984 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #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;
|
||||
|
||||
/**
|
||||
* This acts as a session similar to the <code>java.lang.Process</code>, but
|
||||
* logs the system standard and error streams.
|
||||
* <p>
|
||||
* The bean can be configured to execute a command directly, or be given a map
|
||||
* of commands keyed by the <i>os.name</i> Java system property. In this map,
|
||||
* the default key that is used when no match is found is the
|
||||
* <b>{@link #KEY_OS_DEFAULT *}</b> key.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* Commands may use placeholders, e.g.
|
||||
* <pre><code>
|
||||
* find
|
||||
* -name
|
||||
* ${filename}
|
||||
* </code></pre>
|
||||
* The <b>filename</b> 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.
|
||||
* <p>
|
||||
* Sometimes, a variable may contain several arguments. . In this case, the arguments
|
||||
* need to be tokenized using a standard <tt>StringTokenizer</tt>. To force tokenization
|
||||
* of a value, use:
|
||||
* <pre><code>
|
||||
* SPLIT:${userArgs}
|
||||
* </code></pre>
|
||||
* You should not use this just to split up arguments that are known to require tokenization
|
||||
* up front. The <b>SPLIT:</b> directive works for the entire argument and will not do anything
|
||||
* if it is not at the beginning of the argument. Do not use <b>SPLIT:</b> to break up arguments
|
||||
* that are fixed, so avoid doing this:
|
||||
* <pre><code>
|
||||
* SPLIT:ls -lih
|
||||
* </code></pre>
|
||||
* Instead, break the command up explicitly:
|
||||
* <pre><code>
|
||||
* ls
|
||||
* -lih
|
||||
* </code></pre>
|
||||
*
|
||||
* 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: <b>*</b>
|
||||
*/
|
||||
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<String, String> defaultProperties = emptyMap();
|
||||
private String[] processProperties;
|
||||
private File processDirectory;
|
||||
private final Set<Integer> 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 <i>out</i> and <i>err</i> 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 <tt>true</tt> (default) is to wait for the command to exit,
|
||||
* or <tt>false</tt> 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 <i>os.name</i> 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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 <b>SPLIT:</b> 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:<br>
|
||||
* '<b>SPLIT: dir . ..</b>' becomes '<b>dir</b>', '<b>.</b>' and '<b>..</b>'.<br>
|
||||
* '<b>SPLIT: dir ${path}</b>' (if path is '<b>. ..</b>') becomes '<b>dir</b>', '<b>.</b>' and '<b>..</b>'.<br>
|
||||
* The splitting occurs post-subtitution. Where the arguments are known, it is advisable to avoid
|
||||
* <b>SPLIT:</b>.
|
||||
*
|
||||
* @param commandsByOS a map of command string arrays, keyed by operating system names
|
||||
* @see #setDefaultProperties(Map)
|
||||
* @since 3.0
|
||||
*/
|
||||
public void setCommandsAndArguments(Map<String, String[]> 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 <i>os.name</i> 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<String, String> 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<String, String[]> fixed = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, String> 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.
|
||||
* <p>
|
||||
* <code>null</code> properties will be treated as an empty string for substitution
|
||||
* purposes.
|
||||
*
|
||||
* @param defaultProperties property values
|
||||
*/
|
||||
public void setDefaultProperties(Map<String, String> defaultProperties)
|
||||
{
|
||||
this.defaultProperties = defaultProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set additional runtime properties (environment properties) that will used
|
||||
* by the executing process.
|
||||
* <p>
|
||||
* Any keys or properties that start and end with <b>${...}</b> will be removed on the assumption
|
||||
* that these are unset properties. <tt>null</tt> 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<String, String> processProperties)
|
||||
{
|
||||
ArrayList<String> processPropList = new ArrayList<>(processProperties.size());
|
||||
boolean hasPath = false;
|
||||
String systemPath = System.getenv("PATH");
|
||||
for (Map.Entry<String, String> 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.
|
||||
* <p>
|
||||
* If the value is an unsubsititued variable (<b>${...}</b>) 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 <b>"1, 2"</b>.
|
||||
*
|
||||
* @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.
|
||||
* <code>null</code> properties will be treated as an empty string for substitution
|
||||
* purposes.
|
||||
* @return Returns the full execution results
|
||||
*/
|
||||
public ExecutionResult execute(Map<String, String> 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.
|
||||
* <code>null</code> 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<String, String> 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<String, String> envVariables = System.getenv();
|
||||
for (Map.Entry<String, String> 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.
|
||||
* <p>
|
||||
* <code>null</code> 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<String, String> properties)
|
||||
{
|
||||
Map<String, String> 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<String> adjustedCommandElements = new ArrayList<>(20);
|
||||
for (String s : command)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder(s);
|
||||
for (Map.Entry<String, String> 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<String> 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<Integer> errCodes;
|
||||
private final int exitValue;
|
||||
private final String stdOut;
|
||||
private final String stdErr;
|
||||
|
||||
/**
|
||||
* @param process the process attached to Java - <tt>null</tt> is allowed
|
||||
*/
|
||||
private ExecutionResult(
|
||||
final Process process,
|
||||
final String[] command,
|
||||
final Set<Integer> 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 <tt>false</tt> then the process may still be running when this result is returned.
|
||||
*
|
||||
* @return <tt>true</tt> if the process was killed, otherwise <tt>false</tt>
|
||||
*/
|
||||
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 <code>InputStream</code> and writes it into a
|
||||
* <code>StringBuffer</code>
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* <b>Remember to <code>start</code> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.fs;
|
||||
|
||||
import org.alfresco.transform.base.logging.LogEntry;
|
||||
import org.alfresco.transform.common.ExtensionService;
|
||||
import org.alfresco.transform.exceptions.TransformException;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.util.UriUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
|
||||
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
|
||||
import static org.alfresco.transform.common.ExtensionService.getExtensionForMimetype;
|
||||
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;
|
||||
|
||||
public class FileManager
|
||||
{
|
||||
public static final String SOURCE_FILE = "sourceFile";
|
||||
public static final String TARGET_FILE = "targetFile";
|
||||
|
||||
private FileManager()
|
||||
{
|
||||
}
|
||||
|
||||
public static File createSourceFile(HttpServletRequest request, InputStream inputStream, String sourceMimetype)
|
||||
{
|
||||
try
|
||||
{
|
||||
String extension = "."+getExtensionForMimetype(sourceMimetype);
|
||||
File file = TempFileProvider.createTempFile("source_", extension);
|
||||
Files.copy(inputStream, file.toPath(), REPLACE_EXISTING);
|
||||
if (request != null)
|
||||
{
|
||||
request.setAttribute(SOURCE_FILE, file);
|
||||
}
|
||||
LogEntry.setSource(file.getName(), file.length());
|
||||
return file;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new TransformException(INSUFFICIENT_STORAGE, "Failed to store the source file", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static File createTargetFile(HttpServletRequest request, String sourceMimetype, String targetMimetype)
|
||||
{
|
||||
try
|
||||
{
|
||||
String extension = "."+ExtensionService.getExtensionForTargetMimetype(targetMimetype, sourceMimetype);
|
||||
File file = TempFileProvider.createTempFile("target_", extension);
|
||||
if (request != null)
|
||||
{
|
||||
request.setAttribute(TARGET_FILE, file);
|
||||
}
|
||||
LogEntry.setTarget(file.getName());
|
||||
return file;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new TransformException(INSUFFICIENT_STORAGE, "Failed to create the target file", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void deleteFile(final File file) throws Exception
|
||||
{
|
||||
if (!file.delete())
|
||||
{
|
||||
throw new Exception("Failed to delete file");
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
"Could not read the target file: " + file.getPath());
|
||||
}
|
||||
}
|
||||
catch (MalformedURLException e)
|
||||
{
|
||||
throw new TransformException(INTERNAL_SERVER_ERROR,
|
||||
"The target filename was malformed: " + file.getPath(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static InputStream getMultipartFileInputStream(MultipartFile sourceMultipartFile)
|
||||
{
|
||||
InputStream inputStream;
|
||||
if (sourceMultipartFile == null)
|
||||
{
|
||||
throw new TransformException(BAD_REQUEST, "Required request part 'file' is not present");
|
||||
}
|
||||
try
|
||||
{
|
||||
inputStream = sourceMultipartFile.getInputStream();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new TransformException(BAD_REQUEST, "Unable to read the sourceMultipartFile.", e);
|
||||
}
|
||||
return inputStream;
|
||||
}
|
||||
|
||||
public static InputStream getDirectAccessUrlInputStream(String directUrl)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new URL(directUrl).openStream();
|
||||
}
|
||||
catch (IllegalArgumentException e)
|
||||
{
|
||||
throw new TransformException(BAD_REQUEST, "Direct Access Url is invalid.", e);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new TransformException(BAD_REQUEST, "Direct Access Url not found.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void copyFileToOutputStream(File targetFile, OutputStream outputStream)
|
||||
{
|
||||
try
|
||||
{
|
||||
Files.copy(targetFile.toPath(), outputStream);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new TransformException(INTERNAL_SERVER_ERROR, "Failed to copy targetFile to outputStream.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
public static void deleteFile(HttpServletRequest request, String attributeName)
|
||||
{
|
||||
File file = (File) request.getAttribute(attributeName);
|
||||
if (file != null)
|
||||
{
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
public static ResponseEntity<Resource> createAttachment(String targetFilename, File targetFile)
|
||||
{
|
||||
Resource targetResource = load(targetFile);
|
||||
// targetFilename should never be null (will be "transform."+<something>), so we should not worry about encodePath(null)
|
||||
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
|
||||
{
|
||||
private 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())
|
||||
{
|
||||
throw new RuntimeException("Failed to create temp directory: " + tempDir);
|
||||
}
|
||||
|
||||
return tempDir;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* #%L
|
||||
* Alfresco Transform Core
|
||||
* %%
|
||||
* Copyright (C) 2022 - 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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.html;
|
||||
|
||||
import org.alfresco.transform.config.TransformOption;
|
||||
import org.alfresco.transform.config.TransformOptionGroup;
|
||||
import org.alfresco.transform.config.TransformOptionValue;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* Used in the html test page, which provides a list of known transform option names.
|
||||
*/
|
||||
@Component
|
||||
public class OptionsHelper
|
||||
{
|
||||
private OptionsHelper()
|
||||
{
|
||||
}
|
||||
|
||||
public static Set<String> getOptionNames(Map<String, Set<TransformOption>> transformOptionsByName)
|
||||
{
|
||||
Set<String> set = new TreeSet<>();
|
||||
transformOptionsByName.forEach(((optionName, optionSet) ->
|
||||
optionSet.stream().forEach(option -> addOption(set, option))));
|
||||
return set;
|
||||
}
|
||||
|
||||
private static void addOption(Set<String> set, TransformOption option)
|
||||
{
|
||||
if (option instanceof TransformOptionGroup)
|
||||
{
|
||||
addGroup(set, (TransformOptionGroup)option);
|
||||
}
|
||||
else
|
||||
{
|
||||
addValue(set, (TransformOptionValue)option);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addGroup(Set<String> set, TransformOptionGroup group)
|
||||
{
|
||||
group.getTransformOptions().stream().forEach(option -> addOption(set, option));
|
||||
}
|
||||
|
||||
private static void addValue(Set<String> set, TransformOptionValue value)
|
||||
{
|
||||
set.add(value.getName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.html;
|
||||
|
||||
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.springframework.web.servlet.AsyncHandlerInterceptor;
|
||||
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
|
||||
|
||||
/**
|
||||
* Cleans up temporary files in transform requests that upload the content and download the result.
|
||||
*/
|
||||
public class TransformInterceptor implements AsyncHandlerInterceptor
|
||||
{
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request,
|
||||
HttpServletResponse response, Object handler, Exception ex)
|
||||
{
|
||||
deleteFile(request, SOURCE_FILE);
|
||||
deleteFile(request, TARGET_FILE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.logging;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
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 static java.lang.Math.max;
|
||||
import static org.springframework.http.HttpStatus.OK;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
private static final AtomicInteger count = new AtomicInteger(0);
|
||||
private static final Deque<LogEntry> 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<LogEntry> 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 (StringUtils.isNotBlank(value) && !"0bytes".equals(value))
|
||||
{
|
||||
sb.append(value);
|
||||
sb.append(' ');
|
||||
}
|
||||
}
|
||||
|
||||
public static Collection<LogEntry> 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 void setStatusCodeAndMessage(HttpStatus status, String message)
|
||||
{
|
||||
LogEntry logEntry = currentLogEntry.get();
|
||||
logEntry.statusCode = status.value();
|
||||
logEntry.message = message;
|
||||
logEntry.durationTransform = System.currentTimeMillis() - logEntry.start - logEntry.durationStreamIn;
|
||||
}
|
||||
|
||||
public static long getTransformDuration()
|
||||
{
|
||||
return currentLogEntry.get().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)
|
||||
{
|
||||
return size == -1 ? "" : size(size, "1 byte",
|
||||
new String[]{"bytes", " KB", " MB", " GB", " TB"},
|
||||
new long[]{1024, 1024 * 1024, 1024 * 1024 * 1024, 1024L * 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.logging;
|
||||
|
||||
public final class StandardMessages
|
||||
{
|
||||
private StandardMessages()
|
||||
{
|
||||
}
|
||||
|
||||
public static String COMMUNITY_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";
|
||||
|
||||
public static String ENTERPRISE_LICENCE =
|
||||
"This image is only intended to be used with the Alfresco Enterprise Content Repository which is covered by\n" +
|
||||
"https://www.alfresco.com/legal/agreements and https://www.alfresco.com/terms-use\n" +
|
||||
"\n" +
|
||||
"License rights for this program may be obtained from Alfresco Software, Ltd. pursuant to a written agreement\n" +
|
||||
"and any use of this program without such an agreement is prohibited.\n" +
|
||||
"\n";
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.messaging;
|
||||
|
||||
import org.alfresco.transform.messages.TransformRequestValidator;
|
||||
import org.apache.activemq.command.ActiveMQQueue;
|
||||
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;
|
||||
|
||||
import javax.jms.ConnectionFactory;
|
||||
import javax.jms.Queue;
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
@Override
|
||||
public void configureJmsListeners(@NonNull JmsListenerEndpointRegistrar registrar)
|
||||
{
|
||||
registrar.setMessageHandlerMethodFactory(methodFactory());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DefaultMessageHandlerMethodFactory methodFactory()
|
||||
{
|
||||
DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory();
|
||||
factory.setValidator(new TransformRequestValidator());
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DefaultJmsListenerContainerFactory jmsListenerContainerFactory(
|
||||
final ConnectionFactory connectionFactory,
|
||||
final TransformMessageConverter transformMessageConverter,
|
||||
final MessagingErrorHandler messagingErrorHandler)
|
||||
{
|
||||
final DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
|
||||
factory.setConnectionFactory(connectionFactory);
|
||||
factory.setMessageConverter(transformMessageConverter);
|
||||
factory.setErrorHandler(messagingErrorHandler);
|
||||
factory.setTransactionManager(transactionManager(connectionFactory));
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PlatformTransactionManager transactionManager(final ConnectionFactory connectionFactory)
|
||||
{
|
||||
final JmsTransactionManager transactionManager = new JmsTransactionManager();
|
||||
transactionManager.setConnectionFactory(connectionFactory);
|
||||
return transactionManager;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Queue engineRequestQueue(
|
||||
@Value("${queue.engineRequestQueue}") String engineRequestQueueValue)
|
||||
{
|
||||
return new ActiveMQQueue(engineRequestQueueValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2015-2022 Alfresco Software, Ltd. All rights reserved.
|
||||
*
|
||||
* License rights for this program may be obtained from Alfresco Software, Ltd.
|
||||
* pursuant to a written agreement and any use of this program without such an
|
||||
* agreement is prohibited.
|
||||
*/
|
||||
|
||||
package org.alfresco.transform.base.messaging;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.ErrorHandler;
|
||||
|
||||
/**
|
||||
* Extensible Error Handler for JMS exceptions
|
||||
*
|
||||
* @author Cezar Leahu
|
||||
*/
|
||||
@Service
|
||||
public class MessagingErrorHandler implements ErrorHandler
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(MessagingErrorHandler.class);
|
||||
|
||||
@Override
|
||||
public void handleError(Throwable t)
|
||||
{
|
||||
logger.error("JMS error: " + t.getMessage(), t);
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
* #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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.messaging;
|
||||
|
||||
import org.alfresco.transform.base.TransformController;
|
||||
import org.alfresco.transform.client.model.TransformReply;
|
||||
import org.alfresco.transform.client.model.TransformRequest;
|
||||
import org.alfresco.transform.exceptions.TransformException;
|
||||
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;
|
||||
|
||||
import javax.jms.Destination;
|
||||
import javax.jms.JMSException;
|
||||
import javax.jms.Message;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
||||
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
@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 replyToQueue;
|
||||
|
||||
try
|
||||
{
|
||||
replyToQueue = msg.getJMSReplyTo();
|
||||
if (replyToQueue == 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> transformRequest;
|
||||
try
|
||||
{
|
||||
transformRequest = convert(msg, correlationId);
|
||||
}
|
||||
catch (TransformException e)
|
||||
{
|
||||
logger.error(e.getMessage(), e);
|
||||
replyWithError(replyToQueue, HttpStatus.valueOf(e.getStatus().value()),
|
||||
e.getMessage(), correlationId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!transformRequest.isPresent())
|
||||
{
|
||||
logger.error("T-Request from message with correlationID {} is null!", correlationId);
|
||||
replyWithInternalSvErr(replyToQueue,
|
||||
"JMS exception during T-Request deserialization: ", correlationId);
|
||||
return;
|
||||
}
|
||||
|
||||
transformController.transform(transformRequest.get(), null, replyToQueue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<TransformRequest> 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, message + e.getMessage());
|
||||
}
|
||||
catch (JMSException e)
|
||||
{
|
||||
String message =
|
||||
"JMSException during T-Request deserialization of message with correlationID "
|
||||
+ correlationId + ": ";
|
||||
throw new TransformException(INTERNAL_SERVER_ERROR, message + e.getMessage());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
String message =
|
||||
"Exception during T-Request deserialization of message with correlationID "
|
||||
+ correlationId + ": ";
|
||||
throw new TransformException(INTERNAL_SERVER_ERROR, 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 replyToQueue, final HttpStatus status,
|
||||
final String msg,
|
||||
final String correlationId)
|
||||
{
|
||||
final TransformReply reply = TransformReply
|
||||
.builder()
|
||||
.withStatus(status.value())
|
||||
.withErrorDetails(msg)
|
||||
.build();
|
||||
|
||||
transformReplySender.send(replyToQueue, reply, correlationId);
|
||||
}
|
||||
|
||||
private static String tryRetrieveCorrelationId(final Message msg)
|
||||
{
|
||||
try
|
||||
{
|
||||
return msg.getJMSCorrelationID();
|
||||
}
|
||||
catch (Exception ignore)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
package org.alfresco.transform.base.messaging;
|
||||
|
||||
import com.fasterxml.jackson.databind.JavaType;
|
||||
import com.fasterxml.jackson.databind.type.TypeFactory;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import org.alfresco.transform.client.model.TransformReply;
|
||||
import org.alfresco.transform.client.model.TransformRequest;
|
||||
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 javax.jms.JMSException;
|
||||
import javax.jms.Message;
|
||||
import javax.jms.Session;
|
||||
|
||||
/**
|
||||
* Copied from the t-router. We would need to create a common dependency between t-engine base and t-router that
|
||||
* knows about jms to remove this duplication.
|
||||
*
|
||||
* @author Cezar Leahu
|
||||
*/
|
||||
@Service
|
||||
public class TransformMessageConverter implements MessageConverter
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #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;
|
||||
|
||||
/**
|
||||
* Copied from the t-router.
|
||||
*
|
||||
* @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)
|
||||
{
|
||||
if (destination != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,603 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.metadata;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.alfresco.transform.base.CustomTransformer;
|
||||
import org.alfresco.transform.base.TransformManager;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
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;
|
||||
|
||||
import static org.alfresco.transform.base.metadata.AbstractMetadataExtractorEmbedder.Type.EMBEDDER;
|
||||
|
||||
/**
|
||||
* Helper methods for metadata extract and embed.
|
||||
* <p>
|
||||
* <i>Much of the code is based on AbstractMappingMetadataExtracter from the
|
||||
* content repository. The code has been simplified to only set up mapping one way.</i>
|
||||
* <p>
|
||||
* If a transform specifies that it can convert from {@code "<MIMETYPE>"} to {@code "alfresco-metadata-extract"}
|
||||
* (specified in the {@code engine_config.json}), it is indicating that it can extract metadata from {@code <MIMETYPE>}.
|
||||
*
|
||||
* The transform results in a Map of extracted properties encoded as json being returned to the content repository.
|
||||
* <ul>
|
||||
* <li>The method extracts ALL available metadata from the document with
|
||||
* {@link #extractMetadata(String, InputStream, String, OutputStream, Map, TransformManager)} and then calls
|
||||
* {@link #mapMetadataAndWrite(OutputStream, Map, Map)}.</li>
|
||||
* <li>Selected values from the available metadata are mapped into content repository property names and values,
|
||||
* depending on what is defined in a {@code "<classname>_metadata_extract.properties"} file.</li>
|
||||
* <li>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.</li>
|
||||
* </ul>
|
||||
* To support the same functionality as metadata extractors configured inside the content repository,
|
||||
* extra key value pairs may be returned from {@link #extractMetadata(String, InputStream, String, OutputStream, Map, TransformManager)}.
|
||||
* These are:
|
||||
* <ul>
|
||||
* <li>{@code "sys:overwritePolicy"} which can specify the
|
||||
* {@code org.alfresco.repo.content.metadata.MetadataExtracter.OverwritePolicy} name. Defaults to "PRAGMATIC".</li>
|
||||
* <li>{@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.</li>
|
||||
* <li>{@code "sys:carryAspectProperties"} </li>
|
||||
* <li>{@code "sys:stringTaggingSeparators"} </li>
|
||||
* </ul>
|
||||
*
|
||||
* If a transform specifies that it can convert from {@code "<MIMETYPE>"} to {@code "alfresco-metadata-embed"}, it is
|
||||
* indicating that it can embed metadata in {@code <MIMETYPE>}.
|
||||
*
|
||||
* The transform calls {@link #embedMetadata(String, InputStream, String, OutputStream, Map, TransformManager)}
|
||||
* which should 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 AbstractMetadataExtractorEmbedder implements CustomTransformer
|
||||
{
|
||||
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<String> 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<String, Set<String>> defaultExtractMapping;
|
||||
private final ThreadLocal<Map<String, Set<String>>> extractMapping = new ThreadLocal<>();
|
||||
private Map<String, Set<String>> embedMapping;
|
||||
|
||||
public enum Type
|
||||
{
|
||||
EXTRACTOR, EMBEDDER
|
||||
}
|
||||
|
||||
private final Type type;
|
||||
|
||||
protected AbstractMetadataExtractorEmbedder(Type type, Logger logger)
|
||||
{
|
||||
this.type = type;
|
||||
this.logger = logger;
|
||||
defaultExtractMapping = Collections.emptyMap();
|
||||
embedMapping = Collections.emptyMap();
|
||||
try
|
||||
{
|
||||
defaultExtractMapping = buildExtractMapping();
|
||||
embedMapping = buildEmbedMapping();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.error("Failed to read config", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTransformerName()
|
||||
{
|
||||
return getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transform(String sourceMimetype, InputStream inputStream,
|
||||
String targetMimetype, OutputStream outputStream,
|
||||
Map<String, String> transformOptions, TransformManager transformManager) throws Exception
|
||||
{
|
||||
if (type == EMBEDDER)
|
||||
{
|
||||
embedMetadata(sourceMimetype, inputStream, targetMimetype, outputStream, transformOptions, transformManager);
|
||||
}
|
||||
else
|
||||
{
|
||||
extractMapAndWriteMetadata(sourceMimetype, inputStream, targetMimetype, outputStream, transformOptions, transformManager);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void embedMetadata(String sourceMimetype, InputStream inputStream, String targetMimetype,
|
||||
OutputStream outputStream, Map<String, String> transformOptions, TransformManager transformManager)
|
||||
throws Exception;
|
||||
|
||||
protected Map<String, Serializable> getMetadata(Map<String, String> transformOptions)
|
||||
{
|
||||
String metadataAsJson = transformOptions.get(METADATA);
|
||||
if (metadataAsJson == null)
|
||||
{
|
||||
throw new IllegalArgumentException("No metadata in embed request");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
TypeReference<HashMap<String, Serializable>> typeRef = new TypeReference<>() {};
|
||||
HashMap<String, Serializable> systemProperties = jsonObjectMapper.readValue(metadataAsJson, typeRef);
|
||||
return mapSystemToRaw(systemProperties);
|
||||
}
|
||||
catch (JsonProcessingException e)
|
||||
{
|
||||
throw new IllegalArgumentException("Failed to read metadata from request", e);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Serializable> mapSystemToRaw(Map<String, Serializable> systemMetadata)
|
||||
{
|
||||
Map<String, Serializable> metadataProperties = new HashMap<>(systemMetadata.size() * 2 + 1);
|
||||
for (Map.Entry<String, Serializable> entry : systemMetadata.entrySet())
|
||||
{
|
||||
String modelProperty = entry.getKey();
|
||||
// Check if there is a mapping for this
|
||||
if (!embedMapping.containsKey(modelProperty))
|
||||
{
|
||||
// No mapping - ignore
|
||||
continue;
|
||||
}
|
||||
Serializable documentValue = entry.getValue();
|
||||
Set<String> metadataKeys = embedMapping.get(modelProperty);
|
||||
for (String metadataKey : metadataKeys)
|
||||
{
|
||||
metadataProperties.put(metadataKey, documentValue);
|
||||
}
|
||||
}
|
||||
// Done
|
||||
if (logger.isDebugEnabled())
|
||||
{
|
||||
logger.debug(
|
||||
"Converted system model values to metadata values: \n" +
|
||||
" System Properties: {}\n" +
|
||||
" Metadata Properties: {}", systemMetadata, metadataProperties);
|
||||
}
|
||||
return metadataProperties;
|
||||
}
|
||||
|
||||
protected Map<String, Set<String>> getExtractMapping()
|
||||
{
|
||||
return Collections.unmodifiableMap(extractMapping.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on AbstractMappingMetadataExtracter#getDefaultMapping.
|
||||
*
|
||||
* This method provides a <i>mapping</i> of where to store the values extracted from the documents. The list of
|
||||
* properties need <b>not</b> include all metadata values extracted from the document. This mapping should be
|
||||
* defined in a file based on the class name: {@code "<classname>_metadata_extract.properties"}
|
||||
* @return Returns a static mapping. It may not be null.
|
||||
*/
|
||||
private Map<String, Set<String>> buildExtractMapping()
|
||||
{
|
||||
String filename = getPropertiesFilename(EXTRACT);
|
||||
Properties properties = readProperties(filename);
|
||||
if (properties == null)
|
||||
{
|
||||
logger.error("Failed to read {}", filename);
|
||||
}
|
||||
|
||||
Map<String, String> namespacesByPrefix = getNamespaces(properties);
|
||||
return buildExtractMapping(properties, namespacesByPrefix);
|
||||
}
|
||||
|
||||
private Map<String, Set<String>> buildExtractMapping(Properties properties, Map<String, String> namespacesByPrefix)
|
||||
{
|
||||
// Create the mapping
|
||||
Map<String, Set<String>> convertedMapping = new HashMap<>(17);
|
||||
for (Map.Entry<Object, Object> entry : properties.entrySet())
|
||||
{
|
||||
String documentProperty = (String) entry.getKey();
|
||||
String qnamesStr = (String) entry.getValue();
|
||||
if (documentProperty.startsWith(NAMESPACE_PROPERTY_PREFIX))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Create the entry
|
||||
Set<String> qnames = new HashSet<>(3);
|
||||
convertedMapping.put(documentProperty, qnames);
|
||||
// The to value can be a list of QNames
|
||||
StringTokenizer tokenizer = new StringTokenizer(qnamesStr, ",");
|
||||
while (tokenizer.hasMoreTokens())
|
||||
{
|
||||
String qnameStr = tokenizer.nextToken().trim();
|
||||
qnameStr = getQNameString(namespacesByPrefix, entry, qnameStr, EXTRACT);
|
||||
qnames.add(qnameStr);
|
||||
}
|
||||
if (logger.isTraceEnabled())
|
||||
{
|
||||
logger.trace("Added mapping from {} to {}", documentProperty, qnames);
|
||||
}
|
||||
}
|
||||
return convertedMapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on AbstractMappingMetadataExtracter#getDefaultEmbedMapping.
|
||||
*
|
||||
* This method provides a <i>mapping</i> of model properties that should be embedded in the content. The list of
|
||||
* properties need <b>not</b> include all properties. This mapping should be defined in a file based on the class
|
||||
* name: {@code "<classname>_metadata_embed.properties"}
|
||||
* <p>
|
||||
* If no {@code "<classname>_metadata_embed.properties"} file is found, a reverse of the
|
||||
* {@code "<classname>_metadata_extract.properties"} will be assumed. A last win approach will be used for handling
|
||||
* duplicates.
|
||||
* @return Returns a static mapping. It may not be null.
|
||||
*/
|
||||
private Map<String, Set<String>> buildEmbedMapping()
|
||||
{
|
||||
String filename = getPropertiesFilename(EMBED);
|
||||
Properties properties = readProperties(filename);
|
||||
|
||||
Map<String, Set<String>> mapping;
|
||||
if (properties != null)
|
||||
{
|
||||
Map<String, String> namespacesByPrefix = getNamespaces(properties);
|
||||
mapping = buildEmbedMapping(properties, namespacesByPrefix);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (logger.isDebugEnabled())
|
||||
{
|
||||
logger.debug("No {}, assuming reverse of extract mapping", filename);
|
||||
}
|
||||
mapping = buildEmbedMappingByReversingExtract();
|
||||
}
|
||||
return mapping;
|
||||
}
|
||||
|
||||
private Map<String, Set<String>> buildEmbedMapping(Properties properties, Map<String, String> namespacesByPrefix)
|
||||
{
|
||||
Map<String, Set<String>> convertedMapping = new HashMap<>(17);
|
||||
for (Map.Entry<Object, Object> entry : properties.entrySet())
|
||||
{
|
||||
String modelProperty = (String) entry.getKey();
|
||||
String metadataKeysString = (String) entry.getValue();
|
||||
if (modelProperty.startsWith(NAMESPACE_PROPERTY_PREFIX))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
modelProperty = getQNameString(namespacesByPrefix, entry, modelProperty, EMBED);
|
||||
String[] metadataKeysArray = metadataKeysString.split(",");
|
||||
Set<String> metadataKeys = new HashSet<String>(metadataKeysArray.length);
|
||||
for (String metadataKey : metadataKeysArray)
|
||||
{
|
||||
metadataKeys.add(metadataKey.trim());
|
||||
}
|
||||
// Create the entry
|
||||
convertedMapping.put(modelProperty, metadataKeys);
|
||||
if (logger.isTraceEnabled())
|
||||
{
|
||||
logger.trace("Added mapping from " + modelProperty + " to " + metadataKeysString);
|
||||
}
|
||||
}
|
||||
return convertedMapping;
|
||||
}
|
||||
|
||||
private Map<String, Set<String>> buildEmbedMappingByReversingExtract()
|
||||
{
|
||||
Map<String, Set<String>> extract = buildExtractMapping();
|
||||
Map<String, Set<String>> mapping;
|
||||
mapping = new HashMap<>(extract.size());
|
||||
for (String metadataKey : extract.keySet())
|
||||
{
|
||||
if (extract.get(metadataKey) != null && extract.get(metadataKey).size() > 0)
|
||||
{
|
||||
String modelProperty = extract.get(metadataKey).iterator().next();
|
||||
Set<String> metadataKeys = mapping.get(modelProperty);
|
||||
if (metadataKeys == null)
|
||||
{
|
||||
metadataKeys = new HashSet<>(1);
|
||||
mapping.put(modelProperty, metadataKeys);
|
||||
}
|
||||
metadataKeys.add(metadataKey);
|
||||
if (logger.isTraceEnabled())
|
||||
{
|
||||
logger.trace("Added mapping from {} to {}", modelProperty, metadataKeys);
|
||||
}
|
||||
}
|
||||
}
|
||||
return mapping;
|
||||
}
|
||||
|
||||
private String getPropertiesFilename(String suffix)
|
||||
{
|
||||
String className = this.getClass().getName();
|
||||
String shortClassName = className.split("\\.")[className.split("\\.").length - 1];
|
||||
shortClassName = shortClassName.replace('$', '-');
|
||||
// The embedder uses the reverse of the extractor's data.
|
||||
shortClassName = shortClassName.replace("Embedder", "Extractor");
|
||||
|
||||
return shortClassName + "_metadata_" + suffix + ".properties";
|
||||
}
|
||||
|
||||
private Properties readProperties(String filename)
|
||||
{
|
||||
Properties properties = null;
|
||||
try
|
||||
{
|
||||
InputStream inputStream = AbstractMetadataExtractorEmbedder.class.getClassLoader().getResourceAsStream(filename);
|
||||
if (inputStream != null)
|
||||
{
|
||||
properties = new Properties();
|
||||
properties.load(inputStream);
|
||||
}
|
||||
}
|
||||
catch (IOException ignore)
|
||||
{
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
|
||||
private Map<String, String> getNamespaces(Properties properties)
|
||||
{
|
||||
Map<String, String> namespacesByPrefix = new HashMap<>(5);
|
||||
for (Map.Entry<Object, Object> entry : properties.entrySet())
|
||||
{
|
||||
String propertyName = (String) entry.getKey();
|
||||
if (propertyName.startsWith(NAMESPACE_PROPERTY_PREFIX))
|
||||
{
|
||||
String prefix = propertyName.substring(17);
|
||||
String namespace = (String) entry.getValue();
|
||||
namespacesByPrefix.put(prefix, namespace);
|
||||
}
|
||||
}
|
||||
return namespacesByPrefix;
|
||||
}
|
||||
|
||||
private String getQNameString(Map<String, String> namespacesByPrefix, Map.Entry<Object, Object> entry, String qnameStr, String type)
|
||||
{
|
||||
// Check if we need to resolve a namespace reference
|
||||
int index = qnameStr.indexOf(NAMESPACE_PREFIX);
|
||||
if (index > -1 && qnameStr.charAt(0) != NAMESPACE_BEGIN)
|
||||
{
|
||||
String prefix = qnameStr.substring(0, index);
|
||||
String suffix = qnameStr.substring(index + 1);
|
||||
// It is prefixed
|
||||
String uri = namespacesByPrefix.get(prefix);
|
||||
if (uri == null)
|
||||
{
|
||||
throw new IllegalArgumentException("No prefix mapping for " + type + " property mapping: \n" +
|
||||
" Extractor: " + this + "\n" +
|
||||
" Mapping: " + entry);
|
||||
}
|
||||
qnameStr = NAMESPACE_BEGIN + uri + NAMESPACE_END + suffix;
|
||||
}
|
||||
return qnameStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a value to the map, conserving null values. Values are converted to null if:
|
||||
* <ul>
|
||||
* <li>it is an empty string value after trimming</li>
|
||||
* <li>it is an empty collection</li>
|
||||
* <li>it is an empty array</li>
|
||||
* </ul>
|
||||
* String values are trimmed before being put into the map.
|
||||
* Otherwise, it is up to the extracter to ensure that the value is a <tt>Serializable</tt>.
|
||||
* It is not appropriate to implicitly convert values in order to make them <tt>Serializable</tt>
|
||||
* - the best conversion method will depend on the value's specific meaning.
|
||||
*
|
||||
* @param key the destination key
|
||||
* @param value the serializable value
|
||||
* @param destination the map to put values into
|
||||
* @return Returns <tt>true</tt> if set, otherwise <tt>false</tt>
|
||||
*/
|
||||
// Copied from the content repository's AbstractMappingMetadataExtracter.
|
||||
protected boolean putRawValue(String key, Serializable value, Map<String, Serializable> destination)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
// Just keep this
|
||||
}
|
||||
else if (value instanceof String)
|
||||
{
|
||||
String valueStr = ((String) value).trim();
|
||||
if (valueStr.length() == 0)
|
||||
{
|
||||
value = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (valueStr.contains("\u0000"))
|
||||
{
|
||||
valueStr = valueStr.replaceAll("\u0000", "");
|
||||
}
|
||||
// Keep the trimmed value
|
||||
value = valueStr;
|
||||
}
|
||||
}
|
||||
else if (value instanceof Collection)
|
||||
{
|
||||
Collection<?> valueCollection = (Collection<?>) value;
|
||||
if (valueCollection.isEmpty())
|
||||
{
|
||||
value = null;
|
||||
}
|
||||
}
|
||||
else if (value.getClass().isArray())
|
||||
{
|
||||
if (Array.getLength(value) == 0)
|
||||
{
|
||||
value = null;
|
||||
}
|
||||
}
|
||||
// It passed all the tests
|
||||
destination.put(key, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void extractMapAndWriteMetadata(String sourceMimetype, InputStream inputStream, String targetMimetype,
|
||||
OutputStream outputStream, Map<String, String> transformOptions, TransformManager transformManager)
|
||||
throws Exception
|
||||
{
|
||||
// Use a ThreadLocal to avoid changing method signatures of methods that currently call getExtractMapping.
|
||||
Map<String, Set<String>> mapping = getExtractMappingFromOptions(transformOptions, defaultExtractMapping);
|
||||
try
|
||||
{
|
||||
extractMapping.set(mapping);
|
||||
Map<String, Serializable> metadata = extractMetadata(sourceMimetype, inputStream, targetMimetype,
|
||||
outputStream, transformOptions, transformManager);
|
||||
mapMetadataAndWrite(outputStream, metadata, mapping);
|
||||
}
|
||||
finally
|
||||
{
|
||||
extractMapping.remove();
|
||||
}
|
||||
}
|
||||
|
||||
public abstract Map<String, Serializable> extractMetadata(String sourceMimetype, InputStream inputStream,
|
||||
String targetMimetype, OutputStream outputStream, Map<String, String> transformOptions,
|
||||
TransformManager transformManager) throws Exception;
|
||||
|
||||
private Map<String, Set<String>> getExtractMappingFromOptions(Map<String, String> transformOptions, Map<String,
|
||||
Set<String>> defaultExtractMapping)
|
||||
{
|
||||
String extractMappingOption = transformOptions.get(EXTRACT_MAPPING);
|
||||
if (extractMappingOption != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
TypeReference<HashMap<String, Set<String>>> typeRef = new TypeReference<>() {};
|
||||
return jsonObjectMapper.readValue(extractMappingOption, typeRef);
|
||||
}
|
||||
catch (JsonProcessingException e)
|
||||
{
|
||||
throw new IllegalArgumentException("Failed to read "+ EXTRACT_MAPPING +" from request", e);
|
||||
}
|
||||
}
|
||||
return defaultExtractMapping;
|
||||
}
|
||||
|
||||
public void mapMetadataAndWrite(OutputStream outputStream, Map<String, Serializable> metadata,
|
||||
Map<String, Set<String>> extractMapping) throws IOException
|
||||
{
|
||||
if (logger.isDebugEnabled())
|
||||
{
|
||||
logger.debug("Raw metadata:");
|
||||
metadata.forEach((k,v) -> logger.debug(" {}={}", k, v));
|
||||
}
|
||||
|
||||
metadata = mapRawToSystem(metadata, extractMapping);
|
||||
writeMetadata(outputStream, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on AbstractMappingMetadataExtracter#mapRawToSystem.
|
||||
*
|
||||
* @param rawMetadata Metadata keyed by document properties
|
||||
* @param extractMapping Mapping between document ans system properties
|
||||
* @return Returns the metadata keyed by the system properties
|
||||
*/
|
||||
private Map<String, Serializable> mapRawToSystem(Map<String, Serializable> rawMetadata,
|
||||
Map<String, Set<String>> extractMapping)
|
||||
{
|
||||
boolean debugEnabled = logger.isDebugEnabled();
|
||||
if (debugEnabled)
|
||||
{
|
||||
logger.debug("Returned metadata:");
|
||||
}
|
||||
Map<String, Serializable> systemProperties = new HashMap<>(rawMetadata.size() * 2 + 1);
|
||||
for (Map.Entry<String, Serializable> entry : rawMetadata.entrySet())
|
||||
{
|
||||
String documentKey = entry.getKey();
|
||||
Serializable documentValue = entry.getValue();
|
||||
if (SYS_PROPERTIES.contains(documentKey))
|
||||
{
|
||||
systemProperties.put(documentKey, documentValue);
|
||||
if (debugEnabled)
|
||||
{
|
||||
logger.debug(" {}={}", documentKey, documentValue);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Check if there is a mapping for this
|
||||
if (!extractMapping.containsKey(documentKey))
|
||||
{
|
||||
// No mapping - ignore
|
||||
continue;
|
||||
}
|
||||
|
||||
Set<String> systemQNames = extractMapping.get(documentKey);
|
||||
for (String systemQName : systemQNames)
|
||||
{
|
||||
if (debugEnabled)
|
||||
{
|
||||
logger.debug(" {}={} ({})", systemQName, documentValue, documentKey);
|
||||
}
|
||||
systemProperties.put(systemQName, documentValue);
|
||||
}
|
||||
}
|
||||
return new TreeMap<>(systemProperties);
|
||||
}
|
||||
|
||||
private void writeMetadata(OutputStream outputStream, Map<String, Serializable> results)
|
||||
throws IOException
|
||||
{
|
||||
jsonObjectMapper.writeValue(outputStream, results);
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.model;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* TODO: Copied from org.alfresco.store.entity (alfresco-shared-file-store). To be discussed
|
||||
*
|
||||
* POJO that represents content reference ({@link java.util.UUID})
|
||||
*/
|
||||
public class FileRefEntity
|
||||
{
|
||||
private String fileRef;
|
||||
|
||||
public FileRefEntity() {}
|
||||
|
||||
public FileRefEntity(String fileRef)
|
||||
{
|
||||
this.fileRef = fileRef;
|
||||
}
|
||||
|
||||
public void setFileRef(String fileRef)
|
||||
{
|
||||
this.fileRef = fileRef;
|
||||
}
|
||||
|
||||
public String getFileRef()
|
||||
{
|
||||
return fileRef;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
FileRefEntity that = (FileRefEntity) o;
|
||||
return Objects.equals(fileRef, that.fileRef);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hash(fileRef);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return fileRef;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.model;
|
||||
|
||||
/**
|
||||
* TODO: Copied from org.alfresco.store.entity (alfresco-shared-file-store). To be discussed
|
||||
*
|
||||
* POJO that describes the ContentRefEntry response, contains {@link FileRefEntity} according to API spec
|
||||
*/
|
||||
public class FileRefResponse
|
||||
{
|
||||
private FileRefEntity entry;
|
||||
|
||||
public FileRefResponse() {}
|
||||
|
||||
public FileRefResponse(FileRefEntity entry)
|
||||
{
|
||||
this.entry = entry;
|
||||
}
|
||||
|
||||
public FileRefEntity getEntry()
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
|
||||
public void setEntry(FileRefEntity entry)
|
||||
{
|
||||
this.entry = entry;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.probes;
|
||||
|
||||
import org.alfresco.transform.base.transform.TransformHandler;
|
||||
import org.alfresco.transform.base.logging.LogEntry;
|
||||
import org.alfresco.transform.exceptions.TransformException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import static org.alfresco.transform.base.fs.FileManager.TempFileProvider.createTempFile;
|
||||
import static org.springframework.http.HttpStatus.INSUFFICIENT_STORAGE;
|
||||
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
import static org.springframework.http.HttpStatus.TOO_MANY_REQUESTS;
|
||||
|
||||
/**
|
||||
* Provides test transformations and the logic used by k8 liveness and readiness probes.
|
||||
*
|
||||
* <p><b>K8s probes</b>: A readiness probe indicates if the pod should accept request. <b>It does not indicate that a
|
||||
* pod is ready after startup</b>. The liveness probe indicates when to kill the pod. <b>Both probes are called
|
||||
* throughout the lifetime of the pod</b> and a <b>liveness probes can take place before a readiness probe.</b> The k8s
|
||||
* <b>initialDelaySeconds field is not fully honoured</b> as it is multiplied by a random number, so is
|
||||
* actually a maximum initial delay in seconds, but could be 0. </p>
|
||||
*
|
||||
* <p>Live and readiness probes do test transforms. The first 6 requests result in a transformation of a small test
|
||||
* file. The average time and size is remembered, but excludes the first one as it is normally slower. This is
|
||||
* used in future requests to discover if transformations are becoming slower or unexpectedly change size.</p>
|
||||
*
|
||||
* <p>If a transform longer than a maximum time, a maximum number of transforms have been performed, a test transform is
|
||||
* an unexpected size or a test transform takes an unexpected time, then a non 200 status code is returned resulting in
|
||||
* k8s terminating the pod. These are controlled by:</p>
|
||||
* <ul>
|
||||
* <li>expectedLength the expected length of the target file after a test transform</li>
|
||||
* <li>plusOrMinus allows for variation in the transformed size - generally caused by dates</li>
|
||||
* <li>livenessPercent allows for variation in transform time. Up to 2 and a half times is not
|
||||
* unreasonable under load</li>
|
||||
* <li>maxTransforms the maximum number of transforms (not just test ones) before a restart is triggered</li>
|
||||
* <li>maxTransformSeconds a maximum time any transform (not just test ones) is allowed to take before
|
||||
* a restart is triggered.</li>
|
||||
* <li>livenessTransformPeriodSeconds The number of seconds between test transforms done for live probes</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class ProbeTransform
|
||||
{
|
||||
private final Logger logger = LoggerFactory.getLogger(ProbeTransform.class);
|
||||
|
||||
private static final int AVERAGE_OVER_TRANSFORMS = 5;
|
||||
private final String sourceFilename;
|
||||
private final String sourceMimetype;
|
||||
private final String targetMimetype;
|
||||
private final Map<String, String> transformOptions;
|
||||
private final long minExpectedLength;
|
||||
private final long maxExpectedLength;
|
||||
|
||||
private int livenessPercent;
|
||||
private long probeCount;
|
||||
private int transCount;
|
||||
private long normalTime;
|
||||
private long maxTime = Long.MAX_VALUE;
|
||||
private long nextTransformTime;
|
||||
|
||||
private final boolean livenessTransformEnabled;
|
||||
private final long livenessTransformPeriod;
|
||||
private final long maxTransformCount;
|
||||
private long maxTransformTime;
|
||||
|
||||
private final AtomicBoolean initialised = new AtomicBoolean(false);
|
||||
private final AtomicBoolean readySent = new AtomicBoolean(false);
|
||||
private final AtomicLong transformCount = new AtomicLong(0);
|
||||
private final AtomicBoolean die = new AtomicBoolean(false);
|
||||
|
||||
public int getLivenessPercent()
|
||||
{
|
||||
return livenessPercent;
|
||||
}
|
||||
|
||||
public long getMaxTime()
|
||||
{
|
||||
return maxTime;
|
||||
}
|
||||
|
||||
public ProbeTransform(String sourceFilename, String sourceMimetype, String targetMimetype, Map<String, String> transformOptions,
|
||||
long expectedLength, long plusOrMinus, int livenessPercent, long maxTransforms, long maxTransformSeconds,
|
||||
long livenessTransformPeriodSeconds)
|
||||
{
|
||||
this.sourceFilename = sourceFilename;
|
||||
this.sourceMimetype = sourceMimetype;
|
||||
this.targetMimetype = targetMimetype;
|
||||
this.transformOptions = new HashMap<>(transformOptions);
|
||||
minExpectedLength = Math.max(0, expectedLength - plusOrMinus);
|
||||
maxExpectedLength = expectedLength + plusOrMinus;
|
||||
|
||||
this.livenessPercent = (int) getPositiveLongEnv("livenessPercent", livenessPercent);
|
||||
maxTransformCount = getPositiveLongEnv("maxTransforms", maxTransforms);
|
||||
maxTransformTime = getPositiveLongEnv("maxTransformSeconds", maxTransformSeconds) * 1000;
|
||||
livenessTransformPeriod = getPositiveLongEnv("livenessTransformPeriodSeconds",
|
||||
livenessTransformPeriodSeconds) * 1000;
|
||||
livenessTransformEnabled = getBooleanEnvVar("livenessTransformEnabled", false);
|
||||
}
|
||||
|
||||
private boolean getBooleanEnvVar(final String name, final boolean defaultValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Boolean.parseBoolean(System.getenv(name));
|
||||
}
|
||||
catch (Exception ignore)
|
||||
{
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private long getPositiveLongEnv(String name, long defaultValue)
|
||||
{
|
||||
long l = -1;
|
||||
String env = System.getenv(name);
|
||||
if (env != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
l = Long.parseLong(env);
|
||||
}
|
||||
catch (NumberFormatException ignore)
|
||||
{
|
||||
}
|
||||
}
|
||||
if (l <= 0)
|
||||
{
|
||||
l = defaultValue;
|
||||
}
|
||||
logger.trace("Probe: {}={}", name, l);
|
||||
return l;
|
||||
}
|
||||
|
||||
// We don't want to be doing test transforms every few seconds, but do want frequent live probes.
|
||||
public String doTransformOrNothing(boolean isLiveProbe, TransformHandler transformHandler)
|
||||
{
|
||||
// If not initialised OR it is a live probe and we are scheduled to to do a test transform.
|
||||
probeCount++;
|
||||
// TODO: update/fix/refactor liveness probes as part of ATS-138
|
||||
if (isLiveProbe && !livenessTransformEnabled)
|
||||
{
|
||||
return doNothing(true);
|
||||
}
|
||||
return (isLiveProbe && livenessTransformPeriod > 0 &&
|
||||
(transCount <= AVERAGE_OVER_TRANSFORMS || nextTransformTime < System.currentTimeMillis()))
|
||||
|| !initialised.get()
|
||||
? doTransform(isLiveProbe, transformHandler)
|
||||
: doNothing(isLiveProbe);
|
||||
}
|
||||
|
||||
private String doNothing(boolean isLiveProbe)
|
||||
{
|
||||
String probeMessage = getProbeMessage(isLiveProbe);
|
||||
String message = "Success - No transform.";
|
||||
if (!isLiveProbe && !readySent.getAndSet(true))
|
||||
{
|
||||
logger.trace("{}{}", probeMessage, message);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
private String doTransform(boolean isLiveProbe, TransformHandler transformHandler)
|
||||
{
|
||||
checkMaxTransformTimeAndCount(isLiveProbe);
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
if (nextTransformTime != 0)
|
||||
{
|
||||
do
|
||||
{
|
||||
nextTransformTime += livenessTransformPeriod;
|
||||
}
|
||||
while (nextTransformTime < start);
|
||||
}
|
||||
|
||||
File sourceFile = getSourceFile(isLiveProbe);
|
||||
File targetFile = getTargetFile();
|
||||
transformHandler.handleProbeRequest(sourceMimetype, targetMimetype, transformOptions, sourceFile, targetFile, this);
|
||||
long time = System.currentTimeMillis() - start;
|
||||
String message = "Transform " + time + "ms";
|
||||
checkTargetFile(targetFile, isLiveProbe);
|
||||
|
||||
recordTransformTime(time);
|
||||
calculateMaxTime(time, isLiveProbe);
|
||||
|
||||
if (time > maxTime)
|
||||
{
|
||||
throw new TransformException(INTERNAL_SERVER_ERROR,
|
||||
getMessagePrefix(isLiveProbe) +
|
||||
message + " which is more than " + livenessPercent +
|
||||
"% slower than the normal value of " + normalTime + "ms");
|
||||
}
|
||||
|
||||
// We don't care if the ready or live probe works out if we are 'ready' to take requests.
|
||||
initialised.set(true);
|
||||
|
||||
checkMaxTransformTimeAndCount(isLiveProbe);
|
||||
|
||||
return getProbeMessage(isLiveProbe) + "Success - "+message;
|
||||
}
|
||||
|
||||
private void checkMaxTransformTimeAndCount(boolean isLiveProbe)
|
||||
{
|
||||
if (die.get())
|
||||
{
|
||||
throw new TransformException(TOO_MANY_REQUESTS,
|
||||
getMessagePrefix(isLiveProbe) + "Transformer requested to die. A transform took " +
|
||||
"longer than " + (maxTransformTime / 1000) + " seconds");
|
||||
}
|
||||
|
||||
if (maxTransformCount > 0 && transformCount.get() > maxTransformCount)
|
||||
{
|
||||
throw new TransformException(TOO_MANY_REQUESTS,
|
||||
getMessagePrefix(isLiveProbe) + "Transformer requested to die. It has performed " +
|
||||
"more than " + maxTransformCount + " transformations");
|
||||
}
|
||||
}
|
||||
|
||||
private File getSourceFile(boolean isLiveProbe)
|
||||
{
|
||||
incrementTransformerCount();
|
||||
File sourceFile = createTempFile("probe_source_", "_" + sourceFilename);
|
||||
try (InputStream inputStream = getClass().getResourceAsStream('/' + sourceFilename))
|
||||
{
|
||||
Files.copy(inputStream, sourceFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new TransformException(INSUFFICIENT_STORAGE,
|
||||
getMessagePrefix(isLiveProbe) + "Failed to store the source file", e);
|
||||
}
|
||||
long length = sourceFile.length();
|
||||
LogEntry.setSource(sourceFile.getName(), length);
|
||||
return sourceFile;
|
||||
}
|
||||
|
||||
private File getTargetFile()
|
||||
{
|
||||
File targetFile = createTempFile("probe_target_", "_" + sourceFilename);
|
||||
LogEntry.setTarget(targetFile.getName());
|
||||
return targetFile;
|
||||
}
|
||||
|
||||
public void recordTransformTime(long time)
|
||||
{
|
||||
if (maxTransformTime > 0 && time > maxTransformTime)
|
||||
{
|
||||
die.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void calculateMaxTime(long time, boolean isLiveProbe)
|
||||
{
|
||||
if (transCount <= AVERAGE_OVER_TRANSFORMS)
|
||||
{
|
||||
// Take the average of the first few transforms as the normal time. The initial transform might be slower
|
||||
// so is ignored. Later ones are not included in case we have a gradual performance problem.
|
||||
String message = getMessagePrefix(isLiveProbe) + "Success - Transform " + time + "ms";
|
||||
if (++transCount > 1)
|
||||
{
|
||||
normalTime = (normalTime * (transCount - 2) + time) / (transCount - 1);
|
||||
maxTime = (normalTime * (livenessPercent + 100)) / 100;
|
||||
|
||||
if ((!isLiveProbe && !readySent.getAndSet(
|
||||
true)) || transCount > AVERAGE_OVER_TRANSFORMS)
|
||||
{
|
||||
nextTransformTime = System.currentTimeMillis() + livenessTransformPeriod;
|
||||
logger.trace("{} - {}ms+{}%={}ms", message, normalTime, livenessPercent, maxTime);
|
||||
}
|
||||
}
|
||||
else if (!isLiveProbe && !readySent.getAndSet(true))
|
||||
{
|
||||
logger.trace(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkTargetFile(File targetFile, boolean isLiveProbe)
|
||||
{
|
||||
String probeMessage = getProbeMessage(isLiveProbe);
|
||||
if (!targetFile.exists() || !targetFile.isFile())
|
||||
{
|
||||
throw new TransformException(INTERNAL_SERVER_ERROR,
|
||||
probeMessage + "Target File \"" + targetFile.getAbsolutePath() + "\" did not exist");
|
||||
}
|
||||
long length = targetFile.length();
|
||||
targetFile.delete();
|
||||
if (length < minExpectedLength || length > maxExpectedLength)
|
||||
{
|
||||
throw new TransformException(INTERNAL_SERVER_ERROR,
|
||||
probeMessage + "Target File \"" + targetFile.getAbsolutePath() +
|
||||
"\" was the wrong size (" + length + "). Needed to be between " +
|
||||
minExpectedLength + " and " + maxExpectedLength);
|
||||
}
|
||||
}
|
||||
|
||||
private String getMessagePrefix(boolean isLiveProbe)
|
||||
{
|
||||
return Long.toString(probeCount) + ' ' + getProbeMessage(isLiveProbe);
|
||||
}
|
||||
|
||||
private String getProbeMessage(boolean isLiveProbe)
|
||||
{
|
||||
return (isLiveProbe ? "Live Probe: " : "Ready Probe: ");
|
||||
}
|
||||
|
||||
public void incrementTransformerCount()
|
||||
{
|
||||
transformCount.incrementAndGet();
|
||||
}
|
||||
|
||||
public void setLivenessPercent(int livenessPercent)
|
||||
{
|
||||
this.livenessPercent = livenessPercent;
|
||||
}
|
||||
|
||||
public long getNormalTime()
|
||||
{
|
||||
return normalTime;
|
||||
}
|
||||
|
||||
public void resetForTesting()
|
||||
{
|
||||
probeCount = 0;
|
||||
transCount = 0;
|
||||
normalTime = 0;
|
||||
maxTime = Long.MAX_VALUE;
|
||||
nextTransformTime = 0;
|
||||
|
||||
initialised.set(false);
|
||||
readySent.set(false);
|
||||
transformCount.set(0);
|
||||
die.set(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* #%L
|
||||
* Alfresco Transform Core
|
||||
* %%
|
||||
* Copyright (C) 2022 - 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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.registry;
|
||||
|
||||
public abstract class AbstractTransformConfigSource implements TransformConfigSource
|
||||
{
|
||||
private final String sortOnName;
|
||||
private final String readFrom;
|
||||
private final String baseUrl;
|
||||
|
||||
protected AbstractTransformConfigSource(String sortOnName, String readFrom, String baseUrl)
|
||||
{
|
||||
this.sortOnName = sortOnName;
|
||||
this.readFrom = readFrom;
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSortOnName()
|
||||
{
|
||||
return sortOnName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getReadFrom()
|
||||
{
|
||||
return readFrom;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBaseUrl()
|
||||
{
|
||||
return baseUrl;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* #%L
|
||||
* Alfresco Transform Core
|
||||
* %%
|
||||
* Copyright (C) 2022 - 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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.registry;
|
||||
|
||||
import org.alfresco.transform.base.CustomTransformer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
public class CustomTransformers
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(CustomTransformers.class);
|
||||
|
||||
@Autowired(required = false)
|
||||
private List<CustomTransformer> customTransformerList;
|
||||
|
||||
private final Map<String, CustomTransformer> customTransformersByName = new HashMap<>();
|
||||
|
||||
@PostConstruct
|
||||
private void initCustomTransformersByName()
|
||||
{
|
||||
if (customTransformerList != null)
|
||||
{
|
||||
customTransformerList.forEach(customTransformer ->
|
||||
customTransformersByName.put(customTransformer.getTransformerName(), customTransformer));
|
||||
|
||||
List<String> nonNullTransformerNames = customTransformerList.stream()
|
||||
.map(CustomTransformer::getTransformerName)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!nonNullTransformerNames.isEmpty())
|
||||
{
|
||||
logger.info("Custom Transformers:");
|
||||
nonNullTransformerNames
|
||||
.stream()
|
||||
.sorted()
|
||||
.map(name -> " "+name)
|
||||
.forEach(logger::debug);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public CustomTransformer get(String name)
|
||||
{
|
||||
CustomTransformer customTransformer = customTransformersByName.get(name);
|
||||
return customTransformer == null ? customTransformersByName.get(null) : customTransformer;
|
||||
}
|
||||
|
||||
public void put(String name, CustomTransformer customTransformer)
|
||||
{
|
||||
customTransformersByName.put(name, customTransformer);
|
||||
}
|
||||
|
||||
public List<CustomTransformer> toList()
|
||||
{
|
||||
return customTransformerList;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* #%L
|
||||
* Alfresco Transform Core
|
||||
* %%
|
||||
* Copyright (C) 2022 - 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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.registry;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "transform.config")
|
||||
public class TransformConfigFiles
|
||||
{
|
||||
// Populated from Spring Boot properties or such as transform.config.file.<filename> or environment variables like
|
||||
// TRANSFORM_CONFIG_FILE_<filename>.
|
||||
private final Map<String, String> files = new HashMap<>();
|
||||
|
||||
public Map<String, String> getFile()
|
||||
{
|
||||
return files;
|
||||
}
|
||||
|
||||
public List<Resource> retrieveResources()
|
||||
{
|
||||
return TransformConfigFromFiles.retrieveResources(files);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* #%L
|
||||
* Alfresco Transform Core
|
||||
* %%
|
||||
* Copyright (C) 2022 - 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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.registry;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.alfresco.transform.base.registry.TransformConfigFromFiles.retrieveResource;
|
||||
|
||||
/**
|
||||
* Similar to {@link TransformConfigFiles} but uses the names historically used by the t-router.
|
||||
*/
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "transformer.routes")
|
||||
public class TransformConfigFilesHistoric
|
||||
{
|
||||
// Populated with file paths from Spring Boot properties such as transformer.routes.additional.<engineName> or
|
||||
// environment variables like TRANSFORMER_ROUTES_ADDITIONAL_<engineName>.
|
||||
private final Map<String, String> additional = new HashMap<>();
|
||||
|
||||
private String TRANSFORMER_ROUTES_FROM_CLASSPATH = "transformer-pipelines.json";
|
||||
|
||||
@Value("${transformer-routes-path}")
|
||||
private String transformerRoutesExternalFile;
|
||||
|
||||
public List<Resource> retrieveResources()
|
||||
{
|
||||
ArrayList<Resource> resources = new ArrayList<>();
|
||||
addStandardConfigIfItExists(resources);
|
||||
resources.addAll(TransformConfigFromFiles.retrieveResources(additional));
|
||||
return resources;
|
||||
}
|
||||
|
||||
private void addStandardConfigIfItExists(ArrayList<Resource> resources)
|
||||
{
|
||||
Resource resource = null;
|
||||
if (transformerRoutesExternalFile != null && !transformerRoutesExternalFile.isBlank())
|
||||
{
|
||||
resource = retrieveResource(transformerRoutesExternalFile);
|
||||
}
|
||||
|
||||
if (resource == null || !resource.exists())
|
||||
{
|
||||
resource = new ClassPathResource(TRANSFORMER_ROUTES_FROM_CLASSPATH);
|
||||
}
|
||||
|
||||
if (resource.exists())
|
||||
{
|
||||
resources.add(resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* #%L
|
||||
* Alfresco Transform Core
|
||||
* %%
|
||||
* Copyright (C) 2022 - 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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.registry;
|
||||
|
||||
import org.alfresco.transform.config.reader.TransformConfigResourceReader;
|
||||
import org.alfresco.transform.config.TransformConfig;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import static java.util.stream.Collectors.toList;
|
||||
|
||||
/**
|
||||
* Makes {@link TransformConfig} from files on the classpath or externally available to the {@link TransformRegistry}.
|
||||
*/
|
||||
@Component
|
||||
public class TransformConfigFromFiles
|
||||
{
|
||||
@Autowired
|
||||
private List<TransformConfigSource> transformConfigSources;
|
||||
@Autowired
|
||||
private TransformConfigFiles transformConfigFiles;
|
||||
@Autowired
|
||||
private TransformConfigFilesHistoric transformConfigFilesHistoric;
|
||||
@Autowired
|
||||
private TransformConfigResourceReader transformConfigResourceReader;
|
||||
@Value("${container.isTRouter}")
|
||||
private boolean isTRouter;
|
||||
|
||||
@PostConstruct
|
||||
public void initFileConfig()
|
||||
{
|
||||
final List<Resource> resources = new ArrayList<>();
|
||||
resources.addAll(transformConfigFiles.retrieveResources());
|
||||
resources.addAll(transformConfigFilesHistoric.retrieveResources());
|
||||
resources.forEach(resource ->
|
||||
{
|
||||
String filename = resource.getFilename();
|
||||
transformConfigSources.add(
|
||||
new AbstractTransformConfigSource(filename, filename, isTRouter ? null : "---")
|
||||
{
|
||||
@Override public TransformConfig getTransformConfig()
|
||||
{
|
||||
return transformConfigResourceReader.read(resource);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static List<Resource> retrieveResources(Map<String, String> additional)
|
||||
{
|
||||
return additional
|
||||
.values()
|
||||
.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isBlank())
|
||||
.map(TransformConfigFromFiles::retrieveResource)
|
||||
.collect(toList());
|
||||
}
|
||||
|
||||
public static Resource retrieveResource(final String filename)
|
||||
{
|
||||
final Resource resource = new FileSystemResource(filename);
|
||||
if (resource.exists())
|
||||
{
|
||||
return resource;
|
||||
}
|
||||
return new ClassPathResource(filename);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* #%L
|
||||
* Alfresco Transform Core
|
||||
* %%
|
||||
* Copyright (C) 2022 - 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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.registry;
|
||||
|
||||
import org.alfresco.transform.base.TransformEngine;
|
||||
import org.alfresco.transform.config.TransformConfig;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Makes {@link TransformConfig} from {@link TransformEngine}s available to the {@link TransformRegistry}.
|
||||
*/
|
||||
@Component
|
||||
public class TransformConfigFromTransformEngines
|
||||
{
|
||||
@Autowired(required = false)
|
||||
private List<TransformEngine> transformEngines;
|
||||
@Autowired
|
||||
private List<TransformConfigSource> transformConfigSources;
|
||||
@Value("${container.isTRouter}")
|
||||
private boolean isTRouter;
|
||||
|
||||
@PostConstruct
|
||||
public void initTransformEngineConfig()
|
||||
{
|
||||
if (transformEngines != null)
|
||||
{
|
||||
transformEngines.stream()
|
||||
.forEach(transformEngine -> {
|
||||
TransformConfig transformConfig = transformEngine.getTransformConfig();
|
||||
if (transformConfig != null) // if not a wrapping TransformEngine like all-in-one
|
||||
{
|
||||
String engineName = transformEngine.getTransformEngineName();
|
||||
transformConfigSources.add(
|
||||
new AbstractTransformConfigSource(engineName, engineName, isTRouter ? null : "---")
|
||||
{
|
||||
@Override public TransformConfig getTransformConfig()
|
||||
{
|
||||
return transformEngine.getTransformConfig();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.alfresco.transform.base.registry;
|
||||
|
||||
import org.alfresco.transform.config.TransformConfig;
|
||||
|
||||
public interface TransformConfigSource
|
||||
{
|
||||
String getSortOnName();
|
||||
|
||||
String getReadFrom();
|
||||
|
||||
String getBaseUrl();
|
||||
|
||||
TransformConfig getTransformConfig();
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.registry;
|
||||
|
||||
import org.alfresco.transform.base.CustomTransformer;
|
||||
import org.alfresco.transform.config.TransformConfig;
|
||||
import org.alfresco.transform.config.TransformOption;
|
||||
import org.alfresco.transform.config.TransformOptionGroup;
|
||||
import org.alfresco.transform.config.TransformOptionValue;
|
||||
import org.alfresco.transform.config.Transformer;
|
||||
import org.alfresco.transform.registry.AbstractTransformRegistry;
|
||||
import org.alfresco.transform.registry.CombinedTransformConfig;
|
||||
import org.alfresco.transform.registry.Origin;
|
||||
import org.alfresco.transform.registry.TransformCache;
|
||||
import org.alfresco.transform.registry.TransformerType;
|
||||
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.context.ApplicationContext;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.retry.annotation.Backoff;
|
||||
import org.springframework.retry.annotation.Recover;
|
||||
import org.springframework.retry.annotation.Retryable;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.util.Collections.emptyMap;
|
||||
import static java.util.Objects.isNull;
|
||||
import static java.util.stream.Collectors.toUnmodifiableMap;
|
||||
import static java.util.stream.Collectors.toUnmodifiableSet;
|
||||
import static org.alfresco.transform.config.CoreVersionDecorator.setCoreVersionOnSingleStepTransformers;
|
||||
import static org.alfresco.transform.registry.TransformerType.FAILOVER_TRANSFORMER;
|
||||
import static org.alfresco.transform.registry.TransformerType.PIPELINE_TRANSFORMER;
|
||||
import static org.springframework.util.CollectionUtils.isEmpty;
|
||||
|
||||
@Service
|
||||
public class TransformRegistry extends AbstractTransformRegistry
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(TransformRegistry.class);
|
||||
|
||||
@Autowired
|
||||
private String coreVersion;
|
||||
@Autowired
|
||||
private List<TransformConfigSource> transformConfigSources;
|
||||
@Value("${container.isTRouter}")
|
||||
private boolean isTRouter;
|
||||
|
||||
// Not autowired - avoids a circular reference in the router - initialised on startup event
|
||||
private List<CustomTransformer> customTransformerList;
|
||||
|
||||
private int previousLogMessageHashCode;
|
||||
|
||||
private static class Data extends TransformCache
|
||||
{
|
||||
private TransformConfig transformConfig;
|
||||
private TransformConfig uncombinedTransformConfig;
|
||||
private Map<String,Origin<Transformer>> transformerByNameMap;
|
||||
|
||||
public TransformConfig getTransformConfig()
|
||||
{
|
||||
return transformConfig;
|
||||
}
|
||||
|
||||
public void setTransformConfig(TransformConfig transformConfig)
|
||||
{
|
||||
this.transformConfig = transformConfig;
|
||||
}
|
||||
|
||||
public TransformConfig getUncombinedTransformConfig()
|
||||
{
|
||||
return uncombinedTransformConfig;
|
||||
}
|
||||
|
||||
public void setUncombinedTransformConfig(TransformConfig uncombinedTransformConfig)
|
||||
{
|
||||
this.uncombinedTransformConfig = uncombinedTransformConfig;
|
||||
}
|
||||
|
||||
public Map<String, Origin<Transformer>> getTransformerByNameMap()
|
||||
{
|
||||
return transformerByNameMap;
|
||||
}
|
||||
|
||||
public void setTransformerByNameMap(Map<String, Origin<Transformer>> transformerByNameMap)
|
||||
{
|
||||
this.transformerByNameMap = transformerByNameMap;
|
||||
}
|
||||
|
||||
public int getTransformCount()
|
||||
{
|
||||
return transformCount;
|
||||
}
|
||||
}
|
||||
|
||||
private Data data = new Data();
|
||||
|
||||
// Ensures that read operations are blocked while config is being updated
|
||||
private final ReadWriteLock configRefreshLock = new ReentrantReadWriteLock();
|
||||
|
||||
@EventListener(ContextRefreshedEvent.class)
|
||||
public void handleContextRefreshedEvent(final ContextRefreshedEvent event)
|
||||
{
|
||||
final ApplicationContext context = event.getApplicationContext();
|
||||
// the local "initEngineConfigs" method has to be called through the Spring proxy
|
||||
context.getBean(TransformRegistry.class).initRegistryOnAppStartup(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the registry on application startup. This allows Components in projects that extend the t-engine base
|
||||
* to use @PostConstruct to add to {@code transformConfigSources}, before the registry is loaded.
|
||||
*/
|
||||
@Async
|
||||
@Retryable(include = {IllegalStateException.class},
|
||||
maxAttemptsExpression = "#{${transform.engine.config.retry.attempts}}",
|
||||
backoff = @Backoff(delayExpression = "#{${transform.engine.config.retry.timeout} * 1000}"))
|
||||
void initRegistryOnAppStartup(final ContextRefreshedEvent event)
|
||||
{
|
||||
customTransformerList = event.getApplicationContext().getBean(CustomTransformers.class).toList();
|
||||
retrieveConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recovery method in case all the retries fail. If not specified, the @Retryable method will cause the application
|
||||
* to stop, which we don't want as the t-engine issue may have been sorted out in an hour when the next scheduled
|
||||
* try is made.
|
||||
*/
|
||||
@Recover
|
||||
void recover(IllegalStateException e)
|
||||
{
|
||||
logger.warn(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the schedule from a spring-boot property
|
||||
*/
|
||||
@Scheduled(cron = "${transform.engine.config.cron}")
|
||||
public void retrieveEngineConfigs()
|
||||
{
|
||||
logger.trace("Refresh TransformRegistry");
|
||||
retrieveConfig();
|
||||
}
|
||||
|
||||
void retrieveConfig()
|
||||
{
|
||||
CombinedTransformConfig combinedTransformConfig = new CombinedTransformConfig();
|
||||
|
||||
transformConfigSources.stream()
|
||||
.sorted(Comparator.comparing(TransformConfigSource::getSortOnName))
|
||||
.forEach(source -> {
|
||||
TransformConfig transformConfig = source.getTransformConfig();
|
||||
setCoreVersionOnSingleStepTransformers(transformConfig, coreVersion);
|
||||
combinedTransformConfig.addTransformConfig(transformConfig, source.getReadFrom(), source.getBaseUrl(),
|
||||
this);
|
||||
});
|
||||
|
||||
TransformConfig uncombinedTransformConfig = combinedTransformConfig.buildTransformConfig();
|
||||
combinedTransformConfig.combineTransformerConfig(this);
|
||||
TransformConfig transformConfig = combinedTransformConfig.buildTransformConfig();
|
||||
Map<String, Origin<Transformer>> transformerByNameMap = combinedTransformConfig.getTransformerByNameMap();
|
||||
concurrentUpdate(combinedTransformConfig, uncombinedTransformConfig, transformConfig, transformerByNameMap);
|
||||
|
||||
logTransformers(uncombinedTransformConfig, transformerByNameMap);
|
||||
}
|
||||
|
||||
private void logTransformers(TransformConfig uncombinedTransformConfig, Map<String, Origin<Transformer>> transformerByNameMap)
|
||||
{
|
||||
if (logger.isInfoEnabled())
|
||||
{
|
||||
Set<String> customTransformerNames = new HashSet<>(customTransformerList == null
|
||||
? Collections.emptySet()
|
||||
: customTransformerList.stream().map(CustomTransformer::getTransformerName).collect(Collectors.toSet()));
|
||||
List<String> nonNullTransformerNames = uncombinedTransformConfig.getTransformers().stream()
|
||||
.map(Transformer::getTransformerName)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
ArrayList<String> logMessages = new ArrayList<>();
|
||||
if (!nonNullTransformerNames.isEmpty())
|
||||
{
|
||||
logMessages.add("Transformers (" + nonNullTransformerNames.size() + ") Transforms (" + getData().getTransformCount()+ "):");
|
||||
nonNullTransformerNames
|
||||
.stream()
|
||||
.sorted(String.CASE_INSENSITIVE_ORDER)
|
||||
.map(name -> {
|
||||
Origin<Transformer> transformerOrigin = transformerByNameMap.get(name);
|
||||
String message = " " + name + (transformerOrigin == null
|
||||
? " -- unavailable: see previous messages"
|
||||
: isTRouter
|
||||
? ""
|
||||
: TransformerType.valueOf(transformerOrigin.get()) == PIPELINE_TRANSFORMER
|
||||
? " -- unavailable: pipeline only available via t-router"
|
||||
: TransformerType.valueOf(transformerOrigin.get()) == FAILOVER_TRANSFORMER
|
||||
? " -- unavailable: failover only available via t-router"
|
||||
: !customTransformerNames.contains(name)
|
||||
? " -- missing: CustomTransformer"
|
||||
: "");
|
||||
customTransformerNames.remove(name);
|
||||
return message;
|
||||
})
|
||||
.forEach(logMessages::add);
|
||||
|
||||
List<String> unusedCustomTransformNames = customTransformerNames.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
if (!unusedCustomTransformNames.isEmpty())
|
||||
{
|
||||
logMessages.add("Unused CustomTransformers (" + unusedCustomTransformNames.size() + ") - name is not in the transform config:");
|
||||
unusedCustomTransformNames
|
||||
.stream()
|
||||
.map(name -> " " + name)
|
||||
.forEach(logMessages::add);
|
||||
}
|
||||
|
||||
int logMessageHashCode = logMessages.hashCode();
|
||||
if (previousLogMessageHashCode != logMessageHashCode)
|
||||
{
|
||||
previousLogMessageHashCode = logMessageHashCode;
|
||||
logMessages.stream().forEach(logger::info);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.debug("Config unchanged");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public TransformConfig getTransformConfig()
|
||||
{
|
||||
Data data = getData();
|
||||
return isTRouter
|
||||
? data.getTransformConfig()
|
||||
: data.getUncombinedTransformConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Returns true if transform information has been loaded.
|
||||
*/
|
||||
public boolean isReadyForTransformRequests()
|
||||
{
|
||||
return getData().getTransforms().size() > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Data getData()
|
||||
{
|
||||
return concurrentRead(() -> data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock for reads while updating, use {@link #concurrentRead} to access locked fields
|
||||
*/
|
||||
private void concurrentUpdate(CombinedTransformConfig combinedTransformConfig,
|
||||
TransformConfig uncombinedTransformConfig, TransformConfig transformConfig,
|
||||
Map<String, Origin<Transformer>> transformerByNameMap)
|
||||
{
|
||||
configRefreshLock.writeLock().lock();
|
||||
try
|
||||
{
|
||||
data = new Data(); // clear data
|
||||
data.setTransformConfig(transformConfig);
|
||||
data.setUncombinedTransformConfig(uncombinedTransformConfig);
|
||||
data.setTransformerByNameMap(transformerByNameMap);
|
||||
combinedTransformConfig.registerCombinedTransformers(this);
|
||||
}
|
||||
finally
|
||||
{
|
||||
configRefreshLock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T concurrentRead(Supplier<T> s)
|
||||
{
|
||||
configRefreshLock.readLock().lock();
|
||||
try
|
||||
{
|
||||
return s.get();
|
||||
}
|
||||
finally
|
||||
{
|
||||
configRefreshLock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void logError(String msg)
|
||||
{
|
||||
logger.error(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void logWarn(String msg)
|
||||
{
|
||||
logger.warn(msg);
|
||||
}
|
||||
|
||||
public Transformer getTransformer(final String sourceMediaType, final Long fileSizeBytes,
|
||||
final String targetMediaType, final Map<String, String> transformOptions)
|
||||
{
|
||||
return concurrentRead(() ->
|
||||
{
|
||||
long fileSize = fileSizeBytes == null ? 0 : fileSizeBytes;
|
||||
String transformerName = findTransformerName(sourceMediaType, fileSize, targetMediaType, transformOptions, null);
|
||||
return getTransformer(transformerName);
|
||||
});
|
||||
}
|
||||
|
||||
public Transformer getTransformer(String transformerName)
|
||||
{
|
||||
return getTransformer(getData(), transformerName);
|
||||
}
|
||||
|
||||
private Transformer getTransformer(Data data, String transformerName)
|
||||
{
|
||||
Origin<Transformer> transformerOrigin = data.getTransformerByNameMap().get(transformerName);
|
||||
return transformerOrigin == null ? null : transformerOrigin.get();
|
||||
}
|
||||
|
||||
public boolean checkSourceSize(String transformerName, String sourceMediaType, Long sourceSize, String targetMediaType)
|
||||
{
|
||||
return Optional.ofNullable(getTransformer(transformerName)).
|
||||
map(transformer -> transformer.getSupportedSourceAndTargetList().stream().
|
||||
filter(supported -> supported.getSourceMediaType().equals(sourceMediaType) &&
|
||||
supported.getTargetMediaType().equals(targetMediaType)).
|
||||
findFirst().
|
||||
map(supported -> supported.getMaxSourceSizeBytes() == -1 ||
|
||||
supported.getMaxSourceSizeBytes() >= sourceSize).
|
||||
orElse(false)).
|
||||
orElse(false);
|
||||
}
|
||||
|
||||
public String getEngineName(String transformerName)
|
||||
{
|
||||
return getData().getTransformerByNameMap().get(transformerName).getReadFrom();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the transform options for a given transformer. In a pipeline there may be options for different steps.
|
||||
*/
|
||||
public Map<String, String> filterOptions(final String transformerName, final Map<String, String> options)
|
||||
{
|
||||
Data data = getData();
|
||||
final Map<String, Set<TransformOption>> configOptions = data.getTransformConfig().getTransformOptions();
|
||||
final Transformer transformer = getTransformer(data, transformerName);
|
||||
if (isNull(transformer) || isEmpty(options) || isEmpty(configOptions))
|
||||
{
|
||||
return emptyMap();
|
||||
}
|
||||
|
||||
final Set<String> knownOptions = transformer.getTransformOptions()
|
||||
.stream()
|
||||
.flatMap(name -> configOptions.get(name).stream())
|
||||
.filter(Objects::nonNull)
|
||||
.flatMap(TransformRegistry::retrieveOptionsStrings)
|
||||
.collect(toUnmodifiableSet());
|
||||
if (isEmpty(knownOptions))
|
||||
{
|
||||
return emptyMap();
|
||||
}
|
||||
|
||||
return options
|
||||
.entrySet()
|
||||
.stream()
|
||||
.filter(e -> knownOptions.contains(e.getKey()))
|
||||
.collect(toUnmodifiableMap(Entry::getKey, Entry::getValue));
|
||||
}
|
||||
|
||||
private static Stream<String> retrieveOptionsStrings(final TransformOption option)
|
||||
{
|
||||
if (option instanceof TransformOptionGroup)
|
||||
{
|
||||
return ((TransformOptionGroup) option)
|
||||
.getTransformOptions()
|
||||
.stream()
|
||||
.flatMap(TransformRegistry::retrieveOptionsStrings);
|
||||
}
|
||||
return Stream.of(((TransformOptionValue) option).getName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.sfs;
|
||||
|
||||
import static org.springframework.http.HttpHeaders.ACCEPT;
|
||||
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
|
||||
import static org.springframework.http.HttpMethod.POST;
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import org.alfresco.transform.exceptions.TransformException;
|
||||
import org.alfresco.transform.base.model.FileRefResponse;
|
||||
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.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.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
/**
|
||||
* Simple Rest client that call Alfresco Shared File Store
|
||||
*/
|
||||
@Service
|
||||
public class SharedFileStoreClient
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(SharedFileStoreClient.class);
|
||||
|
||||
@Value("${filestore-url}")
|
||||
private String url;
|
||||
|
||||
@Autowired
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
private WebClient client;
|
||||
|
||||
@PostConstruct
|
||||
public void init()
|
||||
{
|
||||
client = WebClient.builder().baseUrl(url.endsWith("/") ? url : url + "/")
|
||||
.defaultHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE)
|
||||
.defaultHeader(ACCEPT, APPLICATION_JSON_VALUE)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a file from Shared File Store using given file reference
|
||||
*
|
||||
* @param fileRef File reference
|
||||
* @return ResponseEntity<Resource>
|
||||
*/
|
||||
public ResponseEntity<Resource> retrieveFile(String fileRef)
|
||||
{
|
||||
try
|
||||
{
|
||||
return restTemplate.getForEntity(url + "/" + fileRef,
|
||||
org.springframework.core.io.Resource.class);
|
||||
}
|
||||
catch (HttpClientErrorException e)
|
||||
{
|
||||
throw new TransformException(e.getStatusCode(), 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<String, Object> map = new LinkedMultiValueMap<>();
|
||||
map.add("file", value);
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MULTIPART_FORM_DATA);
|
||||
HttpEntity<LinkedMultiValueMap<String, Object>> requestEntity = new HttpEntity<>(map,
|
||||
headers);
|
||||
ResponseEntity<FileRefResponse> responseEntity = restTemplate
|
||||
.exchange(url, POST, requestEntity, FileRefResponse.class);
|
||||
return responseEntity.getBody();
|
||||
}
|
||||
catch (HttpClientErrorException e)
|
||||
{
|
||||
throw new TransformException(e.getStatusCode(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Async
|
||||
public void asyncDelete(final String fileReference)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.debug(" Deleting intermediate file {}", fileReference);
|
||||
|
||||
client.delete().uri(fileReference)
|
||||
.exchange().block();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.error("Failed to delete intermediate file {}: {}", fileReference, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* #%L
|
||||
* Alfresco Transform Core
|
||||
* %%
|
||||
* Copyright (C) 2022 - 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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.transform;
|
||||
|
||||
import org.alfresco.transform.exceptions.TransformException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
/**
|
||||
* Separation of transform fragments logic from the {@link ProcessHandler} logic and {@link StreamHandler}.
|
||||
*/
|
||||
public abstract class FragmentHandler extends StreamHandler
|
||||
{
|
||||
private boolean methodHasBeenCall;
|
||||
private boolean noMoreFragments;
|
||||
|
||||
protected void initTarget()
|
||||
{
|
||||
}
|
||||
|
||||
public OutputStream respondWithFragment(Integer index, boolean finished) throws IOException
|
||||
{
|
||||
try
|
||||
{
|
||||
if (index == null && !methodHasBeenCall)
|
||||
{
|
||||
throw new TransformException(INTERNAL_SERVER_ERROR, "No fragments were produced");
|
||||
}
|
||||
|
||||
if (index != null && noMoreFragments)
|
||||
{
|
||||
throw new TransformException(INTERNAL_SERVER_ERROR, "Final fragment already sent");
|
||||
}
|
||||
|
||||
if (index != null)
|
||||
{
|
||||
super.handleSuccessfulTransform();
|
||||
logFragment(index, transformManager.getOutputLength());
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
methodHasBeenCall = true;
|
||||
noMoreFragments = noMoreFragments || index == null || finished;
|
||||
}
|
||||
return noMoreFragments ? null : switchToNewOutputStreamForNewFragment();
|
||||
}
|
||||
|
||||
protected void logFragment(Integer index, Long outputLength)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleSuccessfulTransform() throws IOException
|
||||
{
|
||||
if (!methodHasBeenCall)
|
||||
{
|
||||
super.handleSuccessfulTransform();
|
||||
}
|
||||
}
|
||||
|
||||
private OutputStream switchToNewOutputStreamForNewFragment() throws IOException
|
||||
{
|
||||
transformManager.getOutputStream().close();
|
||||
transformManager.deleteTargetFile();
|
||||
initTarget();
|
||||
setOutputStream();
|
||||
return outputStream;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* #%L
|
||||
* Alfresco Transform Core
|
||||
* %%
|
||||
* Copyright (C) 2022 - 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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.transform;
|
||||
|
||||
import org.alfresco.transform.base.CustomTransformer;
|
||||
import org.alfresco.transform.base.TransformController;
|
||||
import org.alfresco.transform.base.logging.LogEntry;
|
||||
import org.alfresco.transform.base.probes.ProbeTransform;
|
||||
import org.alfresco.transform.base.registry.CustomTransformers;
|
||||
import org.alfresco.transform.client.model.TransformRequest;
|
||||
import org.alfresco.transform.exceptions.TransformException;
|
||||
import org.alfresco.transform.common.TransformerDebug;
|
||||
import org.alfresco.transform.registry.TransformServiceRegistry;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.jms.Destination;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.alfresco.transform.common.RequestParamMap.DIRECT_ACCESS_URL;
|
||||
import static org.alfresco.transform.common.RequestParamMap.SOURCE_EXTENSION;
|
||||
import static org.alfresco.transform.common.RequestParamMap.SOURCE_MIMETYPE;
|
||||
import static org.alfresco.transform.common.RequestParamMap.TARGET_EXTENSION;
|
||||
import static org.alfresco.transform.common.RequestParamMap.TARGET_MIMETYPE;
|
||||
import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
||||
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
import static org.springframework.http.HttpStatus.OK;
|
||||
|
||||
/**
|
||||
* Provides the transform logic common to http (upload/download), message and probe requests. See
|
||||
* {@link TransformHandler#handleHttpRequest(HttpServletRequest, MultipartFile, String, String, Map, ProbeTransform)},
|
||||
* {@link TransformHandler#handleMessageRequest(TransformRequest, Long, Destination, ProbeTransform)} and
|
||||
* {@link TransformHandler#handleProbeRequest(String, String, Map, File, File, ProbeTransform)}. Note the handing of transform requests
|
||||
* via a message queue is the same as via the {@link TransformController#transform(TransformRequest, Long, Destination)}.
|
||||
*/
|
||||
abstract class ProcessHandler extends FragmentHandler
|
||||
{
|
||||
private static final List<String> NON_TRANSFORM_OPTION_REQUEST_PARAMETERS = Arrays.asList(SOURCE_EXTENSION,
|
||||
TARGET_EXTENSION, TARGET_MIMETYPE, SOURCE_MIMETYPE, DIRECT_ACCESS_URL);
|
||||
|
||||
protected final String sourceMimetype;
|
||||
protected final String targetMimetype;
|
||||
private final Map<String, String> transformOptions;
|
||||
protected String reference;
|
||||
private final TransformServiceRegistry transformRegistry;
|
||||
private final TransformerDebug transformerDebug;
|
||||
private final ProbeTransform probeTransform;
|
||||
private final CustomTransformers customTransformers;
|
||||
|
||||
ProcessHandler(String sourceMimetype, String targetMimetype, Map<String, String> transformOptions,
|
||||
String reference, TransformServiceRegistry transformRegistry, TransformerDebug transformerDebug,
|
||||
ProbeTransform probeTransform, CustomTransformers customTransformers)
|
||||
{
|
||||
this.sourceMimetype = sourceMimetype;
|
||||
this.targetMimetype = targetMimetype;
|
||||
this.transformOptions = cleanTransformOptions(transformOptions);
|
||||
this.reference = reference;
|
||||
|
||||
this.transformRegistry = transformRegistry;
|
||||
this.transformerDebug = transformerDebug;
|
||||
this.probeTransform = probeTransform;
|
||||
this.customTransformers = customTransformers;
|
||||
}
|
||||
|
||||
private static Map<String, String> cleanTransformOptions(Map<String, String> requestParameters)
|
||||
{
|
||||
Map<String, String> transformOptions = new HashMap<>(requestParameters);
|
||||
NON_TRANSFORM_OPTION_REQUEST_PARAMETERS.forEach(transformOptions.keySet()::remove);
|
||||
transformOptions.values().removeIf(String::isEmpty);
|
||||
return transformOptions;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() throws IOException
|
||||
{
|
||||
transformManager.setProcessHandler(this);
|
||||
super.init();
|
||||
}
|
||||
|
||||
|
||||
public String getReference()
|
||||
{
|
||||
return reference;
|
||||
}
|
||||
|
||||
public void handleTransformRequest()
|
||||
{
|
||||
LogEntry.start();
|
||||
transformManager.setSourceMimetype(sourceMimetype);
|
||||
transformManager.setTargetMimetype(targetMimetype);
|
||||
probeTransform.incrementTransformerCount();
|
||||
try
|
||||
{
|
||||
init();
|
||||
long sourceSizeInBytes = getSourceSize();
|
||||
String transformName = getTransformerName(sourceMimetype, sourceSizeInBytes, targetMimetype, transformOptions);
|
||||
CustomTransformer customTransformer = getCustomTransformer(transformName);
|
||||
transformerDebug.pushTransform(reference, sourceMimetype, targetMimetype, sourceSizeInBytes, transformName);
|
||||
transformerDebug.logOptions(reference, transformOptions);
|
||||
handleTransform(customTransformer);
|
||||
}
|
||||
catch (TransformException e)
|
||||
{
|
||||
transformerDebug.logFailure(reference, " Error: "+e.getMessage());
|
||||
LogEntry.setStatusCodeAndMessage(e.getStatus(), e.getMessage());
|
||||
handleTransformException(e);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
transformerDebug.logFailure(reference, " Error: "+e.getMessage());
|
||||
LogEntry.setStatusCodeAndMessage(INTERNAL_SERVER_ERROR, e.getMessage());
|
||||
handleException(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
long time = LogEntry.getTransformDuration();
|
||||
probeTransform.recordTransformTime(time);
|
||||
transformerDebug.popTransform(reference, time);
|
||||
LogEntry.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void logFragment(Integer index, Long outputLength)
|
||||
{
|
||||
transformerDebug.logFragment(reference, index, outputLength);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transform(CustomTransformer customTransformer) throws Exception
|
||||
{
|
||||
customTransformer.transform(sourceMimetype, inputStream, targetMimetype, outputStream, transformOptions, transformManager);
|
||||
}
|
||||
|
||||
protected abstract long getSourceSize();
|
||||
|
||||
@Override
|
||||
public void onSuccessfulTransform()
|
||||
{
|
||||
sendTransformResponse(transformManager);
|
||||
|
||||
LogEntry.setTargetSize(transformManager.getOutputLength());
|
||||
LogEntry.setStatusCodeAndMessage(OK, "Success");
|
||||
}
|
||||
|
||||
protected void sendTransformResponse(TransformManagerImpl transformManager)
|
||||
{
|
||||
}
|
||||
|
||||
protected void handleTransformException(TransformException e)
|
||||
{
|
||||
throw e;
|
||||
}
|
||||
|
||||
protected void handleException(Exception e)
|
||||
{
|
||||
throw new TransformException(INTERNAL_SERVER_ERROR, e.getMessage(), e);
|
||||
}
|
||||
|
||||
private String getTransformerName(final String sourceMimetype, long sourceSizeInBytes, final String targetMimetype,
|
||||
final Map<String, String> transformOptions)
|
||||
{
|
||||
final String transformerName = transformRegistry.findTransformerName(sourceMimetype,
|
||||
sourceSizeInBytes, targetMimetype, transformOptions, null);
|
||||
if (transformerName == null)
|
||||
{
|
||||
throw new TransformException(BAD_REQUEST, "No transforms for: "+
|
||||
sourceMimetype+" -> "+targetMimetype+transformOptions.entrySet().stream()
|
||||
.map(entry -> entry.getKey()+"="+entry.getValue())
|
||||
.collect(Collectors.joining(", ", " ", "")));
|
||||
}
|
||||
return transformerName;
|
||||
}
|
||||
|
||||
private CustomTransformer getCustomTransformer(String transformName)
|
||||
{
|
||||
CustomTransformer customTransformer = customTransformers.get(transformName);
|
||||
if (customTransformer == null)
|
||||
{
|
||||
throw new TransformException(INTERNAL_SERVER_ERROR, "Custom Transformer "+transformName+" not found");
|
||||
}
|
||||
return customTransformer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* #%L
|
||||
* Alfresco Transform Core
|
||||
* %%
|
||||
* Copyright (C) 2022 - 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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.transform;
|
||||
|
||||
import org.alfresco.transform.base.CustomTransformer;
|
||||
import org.alfresco.transform.base.TransformManager;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* Separation of InputStream, OutputStream, sourceFile and targetFile from the {@link ProcessHandler} logic. Allows
|
||||
* {@link CustomTransformer} implementations to call {@link TransformManager#createSourceFile()} and
|
||||
* {@link TransformManager#createTargetFile()} so that extra Files are not created if there was one already in
|
||||
* existence.
|
||||
*
|
||||
* Subclasses MUST call transformManager.setSourceFile(File) and transformManager.setSourceFile(File) if they start
|
||||
* with files rather than streams, before calling the {@link #init()} method which calls
|
||||
* transformManager.setOutputStream(InputStream) and transformManager.setOutputStream(OutputStream).
|
||||
*/
|
||||
public abstract class StreamHandler
|
||||
{
|
||||
protected TransformManagerImpl transformManager = new TransformManagerImpl();
|
||||
protected InputStream inputStream;
|
||||
protected OutputStream outputStream;
|
||||
|
||||
public abstract void handleTransformRequest() throws Exception;
|
||||
|
||||
protected void init() throws IOException
|
||||
{
|
||||
setInputStream();
|
||||
setOutputStream();
|
||||
}
|
||||
|
||||
private void setInputStream() throws IOException
|
||||
{
|
||||
inputStream = transformManager.setInputStream(getInputStream());
|
||||
}
|
||||
|
||||
protected void setOutputStream() throws IOException
|
||||
{
|
||||
outputStream = transformManager.setOutputStream(getOutputStream());
|
||||
}
|
||||
|
||||
protected abstract InputStream getInputStream() throws IOException;
|
||||
|
||||
protected abstract OutputStream getOutputStream() throws IOException;
|
||||
|
||||
protected void handleTransform(CustomTransformer customTransformer) throws Exception
|
||||
{
|
||||
try
|
||||
{
|
||||
transform(customTransformer);
|
||||
handleSuccessfulTransform();
|
||||
}
|
||||
finally
|
||||
{
|
||||
closeOutputStream();
|
||||
closeInputStreamWithoutException();
|
||||
deleteTmpFiles();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void transform(CustomTransformer customTransformer) throws Exception;
|
||||
|
||||
protected void handleSuccessfulTransform() throws IOException
|
||||
{
|
||||
transformManager.copyTargetFileToOutputStream();
|
||||
onSuccessfulTransform();
|
||||
}
|
||||
|
||||
protected void onSuccessfulTransform()
|
||||
{
|
||||
}
|
||||
|
||||
protected void closeOutputStream() throws IOException
|
||||
{
|
||||
transformManager.getOutputStream().close();
|
||||
}
|
||||
|
||||
private void closeInputStreamWithoutException()
|
||||
{
|
||||
if (inputStream != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
inputStream.close();
|
||||
}
|
||||
catch (IOException ignore)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteTmpFiles()
|
||||
{
|
||||
transformManager.deleteSourceFile();
|
||||
transformManager.deleteTargetFile();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.transform;
|
||||
|
||||
import org.alfresco.transform.base.sfs.SharedFileStoreClient;
|
||||
import org.alfresco.transform.base.messaging.TransformReplySender;
|
||||
import org.alfresco.transform.base.model.FileRefResponse;
|
||||
import org.alfresco.transform.base.probes.ProbeTransform;
|
||||
import org.alfresco.transform.base.registry.CustomTransformers;
|
||||
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.common.ExtensionService;
|
||||
import org.alfresco.transform.exceptions.TransformException;
|
||||
import org.alfresco.transform.common.TransformerDebug;
|
||||
import org.alfresco.transform.messages.TransformRequestValidator;
|
||||
import org.alfresco.transform.messages.TransformStack;
|
||||
import org.alfresco.transform.registry.TransformServiceRegistry;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.validation.DirectFieldBindingResult;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.jms.Destination;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static org.alfresco.transform.base.fs.FileManager.createAttachment;
|
||||
import static org.alfresco.transform.base.fs.FileManager.createTargetFile;
|
||||
import static org.alfresco.transform.base.fs.FileManager.getDirectAccessUrlInputStream;
|
||||
import static org.alfresco.transform.base.fs.FileManager.getMultipartFileInputStream;
|
||||
import static org.alfresco.transform.common.RequestParamMap.DIRECT_ACCESS_URL;
|
||||
import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
||||
import static org.springframework.http.HttpStatus.CREATED;
|
||||
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
/**
|
||||
* Handles the transform requests from either http or a message.
|
||||
*/
|
||||
@Component
|
||||
public class TransformHandler
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(TransformHandler.class);
|
||||
|
||||
private static final String FAILED_WRITING_TO_SFS = "Failed writing to SFS";
|
||||
|
||||
@Autowired(required = false)
|
||||
private CustomTransformers customTransformers;
|
||||
@Autowired
|
||||
private SharedFileStoreClient alfrescoSharedFileStoreClient;
|
||||
@Autowired
|
||||
private TransformRequestValidator transformRequestValidator;
|
||||
@Autowired
|
||||
private TransformServiceRegistry transformRegistry;
|
||||
@Autowired
|
||||
private TransformReplySender transformReplySender;
|
||||
@Autowired
|
||||
private TransformerDebug transformerDebug;
|
||||
|
||||
private final AtomicInteger httpRequestCount = new AtomicInteger(1);
|
||||
|
||||
public ResponseEntity<Resource> handleHttpRequest(HttpServletRequest request,
|
||||
MultipartFile sourceMultipartFile, String sourceMimetype, String targetMimetype,
|
||||
Map<String, String> requestParameters, ProbeTransform probeTransform)
|
||||
{
|
||||
AtomicReference<ResponseEntity<Resource>> responseEntity = new AtomicReference<>();
|
||||
|
||||
new ProcessHandler(sourceMimetype, targetMimetype, requestParameters,
|
||||
"e" + httpRequestCount.getAndIncrement(), transformRegistry,
|
||||
transformerDebug, probeTransform, customTransformers)
|
||||
{
|
||||
@Override
|
||||
protected void init() throws IOException
|
||||
{
|
||||
transformManager.setRequest(request);
|
||||
transformManager.setTargetFile(createTargetFile(request, sourceMimetype, targetMimetype));
|
||||
transformManager.keepTargetFile(); // Will be deleted in TransformInterceptor.afterCompletion()
|
||||
super.init();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InputStream getInputStream()
|
||||
{
|
||||
return getInputStreamForHandleHttpRequest(requestParameters, sourceMultipartFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected OutputStream getOutputStream() throws IOException
|
||||
{
|
||||
return getOutputStreamFromFile(transformManager.getTargetFile());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long getSourceSize()
|
||||
{
|
||||
return sourceMultipartFile == null ? -1 : sourceMultipartFile.getSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void sendTransformResponse(TransformManagerImpl transformManager)
|
||||
{
|
||||
String extension = ExtensionService.getExtensionForTargetMimetype(targetMimetype, sourceMimetype);
|
||||
responseEntity.set(createAttachment("transform."+extension, transformManager.getTargetFile()));
|
||||
}
|
||||
}.handleTransformRequest();
|
||||
|
||||
return responseEntity.get();
|
||||
}
|
||||
|
||||
public void handleProbeRequest(String sourceMimetype, String targetMimetype, Map<String, String> transformOptions,
|
||||
File sourceFile, File targetFile, ProbeTransform probeTransform)
|
||||
{
|
||||
new ProcessHandler(sourceMimetype, targetMimetype, transformOptions,
|
||||
"p" + httpRequestCount.getAndIncrement(), transformRegistry,
|
||||
transformerDebug, probeTransform, customTransformers)
|
||||
{
|
||||
@Override
|
||||
protected void init() throws IOException
|
||||
{
|
||||
transformManager.setSourceFile(sourceFile);
|
||||
transformManager.setTargetFile(targetFile);
|
||||
transformManager.keepTargetFile();
|
||||
super.init();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InputStream getInputStream()
|
||||
{
|
||||
return getInputStreamForHandleProbeRequest(sourceFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long getSourceSize()
|
||||
{
|
||||
return sourceFile.length();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected OutputStream getOutputStream() throws IOException
|
||||
{
|
||||
return getOutputStreamFromFile(targetFile);
|
||||
}
|
||||
}.handleTransformRequest();
|
||||
}
|
||||
|
||||
public TransformReply handleMessageRequest(TransformRequest request, Long timeout, Destination replyToQueue,
|
||||
ProbeTransform probeTransform)
|
||||
{
|
||||
TransformReply reply = createBasicTransformReply(request);
|
||||
new ProcessHandler(request.getSourceMediaType(), request.getTargetMediaType(),
|
||||
request.getTransformRequestOptions(),"unset", transformRegistry,
|
||||
transformerDebug, probeTransform, customTransformers)
|
||||
{
|
||||
@Override
|
||||
protected void init() throws IOException
|
||||
{
|
||||
checkTransformRequestValid(request, reply);
|
||||
reference = TransformStack.getReference(reply.getInternalContext());
|
||||
initTarget();
|
||||
super.init();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initTarget()
|
||||
{
|
||||
transformManager.setTargetFile(createTargetFile(null, sourceMimetype, targetMimetype));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InputStream getInputStream()
|
||||
{
|
||||
return getInputStreamForHandleMessageRequest(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long getSourceSize()
|
||||
{
|
||||
return request.getSourceSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected OutputStream getOutputStream() throws IOException
|
||||
{
|
||||
return getOutputStreamFromFile(transformManager.getTargetFile());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void sendTransformResponse(TransformManagerImpl transformManager)
|
||||
{
|
||||
reply.getInternalContext().setCurrentSourceSize(transformManager.getOutputLength());
|
||||
saveTargetFileInSharedFileStore(transformManager.getTargetFile(), reply);
|
||||
sendSuccessfulResponse(timeout, reply, replyToQueue);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleTransformException(TransformException e)
|
||||
{
|
||||
sendFailedResponse(reply, e, e.getStatus(), replyToQueue);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleException(Exception e)
|
||||
{
|
||||
sendFailedResponse(reply, e, INTERNAL_SERVER_ERROR, replyToQueue);
|
||||
}
|
||||
}.handleTransformRequest();
|
||||
return reply;
|
||||
}
|
||||
|
||||
private void sendSuccessfulResponse(Long timeout, TransformReply reply, Destination replyToQueue)
|
||||
{
|
||||
logger.trace("Sending successful {}, timeout {} ms", reply, timeout);
|
||||
transformReplySender.send(replyToQueue, reply);
|
||||
}
|
||||
|
||||
private void sendFailedResponse(TransformReply reply, Exception e, HttpStatus status, Destination replyToQueue)
|
||||
{
|
||||
reply.setStatus(status.value());
|
||||
reply.setErrorDetails(messageWithCause("Transform failed", e));
|
||||
|
||||
transformerDebug.logFailure(reply);
|
||||
logger.trace("Transform failed. Sending {}", reply, e);
|
||||
transformReplySender.send(replyToQueue, reply);
|
||||
}
|
||||
|
||||
private void checkTransformRequestValid(TransformRequest request, TransformReply reply)
|
||||
{
|
||||
final Errors errors = validateTransformRequest(request);
|
||||
validateInternalContext(request, errors);
|
||||
reply.setInternalContext(request.getInternalContext());
|
||||
|
||||
if (!errors.getAllErrors().isEmpty())
|
||||
{
|
||||
String errorDetails = errors.getAllErrors().stream().map(Object::toString).collect(joining(", "));
|
||||
throw new TransformException(BAD_REQUEST, errorDetails);
|
||||
}
|
||||
}
|
||||
|
||||
private TransformReply createBasicTransformReply(TransformRequest request)
|
||||
{
|
||||
TransformReply reply = new TransformReply();
|
||||
reply.setRequestId(request.getRequestId());
|
||||
reply.setSourceReference(request.getSourceReference());
|
||||
reply.setSchema(request.getSchema());
|
||||
reply.setClientData(request.getClientData());
|
||||
reply.setInternalContext(request.getInternalContext());
|
||||
return reply;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
initialiseContext(request);
|
||||
}
|
||||
|
||||
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 InputStream getSharedFileStoreInputStream(String sourceReference)
|
||||
{
|
||||
ResponseEntity<Resource> responseEntity = alfrescoSharedFileStoreClient.retrieveFile(sourceReference);
|
||||
final Resource body = responseEntity.getBody();
|
||||
if (body == null)
|
||||
{
|
||||
String message = "Source file with reference: " + sourceReference + " is null or empty.";
|
||||
logger.warn(message);
|
||||
throw new TransformException(BAD_REQUEST, message);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return body.getInputStream();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
String message = "Shared File Store reference is invalid.";
|
||||
logger.warn(message);
|
||||
throw new TransformException(BAD_REQUEST, message, e);
|
||||
}
|
||||
}
|
||||
|
||||
private InputStream getInputStreamForHandleHttpRequest(Map<String, String> requestParameters,
|
||||
MultipartFile sourceMultipartFile)
|
||||
{
|
||||
final String directUrl = requestParameters.getOrDefault(DIRECT_ACCESS_URL, "");
|
||||
return new BufferedInputStream(directUrl.isBlank()
|
||||
? getMultipartFileInputStream(sourceMultipartFile)
|
||||
: getDirectAccessUrlInputStream(directUrl));
|
||||
}
|
||||
|
||||
private InputStream getInputStreamForHandleProbeRequest(File sourceFile)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new BufferedInputStream(new FileInputStream(sourceFile));
|
||||
}
|
||||
catch (FileNotFoundException e)
|
||||
{
|
||||
throw new TransformException(INTERNAL_SERVER_ERROR, messageWithCause("Failed to read the probe source", e));
|
||||
}
|
||||
}
|
||||
|
||||
private InputStream getInputStreamForHandleMessageRequest(TransformRequest request)
|
||||
{
|
||||
final String directUrl = request.getTransformRequestOptions().getOrDefault(DIRECT_ACCESS_URL, "");
|
||||
try
|
||||
{
|
||||
return new BufferedInputStream(directUrl.isBlank()
|
||||
? getSharedFileStoreInputStream(request.getSourceReference())
|
||||
: getDirectAccessUrlInputStream(directUrl));
|
||||
}
|
||||
catch (TransformException e)
|
||||
{
|
||||
throw new TransformException(e.getStatus(), messageWithCause("Failed to read the source", e));
|
||||
}
|
||||
catch (HttpClientErrorException e)
|
||||
{
|
||||
throw new TransformException(e.getStatusCode(), messageWithCause("Failed to read the source from the SFS", e));
|
||||
}
|
||||
}
|
||||
|
||||
private OutputStream getOutputStreamFromFile(File targetFile) throws IOException
|
||||
{
|
||||
return new BufferedOutputStream(new FileOutputStream(targetFile));
|
||||
}
|
||||
|
||||
private void saveTargetFileInSharedFileStore(File targetFile, TransformReply reply)
|
||||
{
|
||||
FileRefResponse targetRef;
|
||||
try
|
||||
{
|
||||
targetRef = alfrescoSharedFileStoreClient.saveFile(targetFile);
|
||||
}
|
||||
catch (TransformException e)
|
||||
{
|
||||
throw new TransformException(e.getStatus(), messageWithCause(FAILED_WRITING_TO_SFS, e));
|
||||
}
|
||||
catch (HttpClientErrorException e)
|
||||
{
|
||||
throw new TransformException(e.getStatusCode(), messageWithCause(FAILED_WRITING_TO_SFS, e));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new TransformException(INTERNAL_SERVER_ERROR, messageWithCause(FAILED_WRITING_TO_SFS, e));
|
||||
}
|
||||
|
||||
reply.setTargetReference(targetRef.getEntry().getFileRef());
|
||||
reply.setStatus(CREATED.value());
|
||||
}
|
||||
|
||||
private static String messageWithCause(final String prefix, Throwable e)
|
||||
{
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append(prefix).append(" - ");
|
||||
if (e.getClass() != TransformException.class)
|
||||
{
|
||||
sb.append(e.getClass().getSimpleName()).append(": ");
|
||||
}
|
||||
sb.append(e.getMessage());
|
||||
|
||||
while (e.getCause() != null)
|
||||
{
|
||||
e = e.getCause();
|
||||
sb.append(", cause ")
|
||||
.append(e.getClass().getSimpleName()).append(": ")
|
||||
.append(e.getMessage());
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.transform;
|
||||
|
||||
import org.alfresco.transform.base.TransformManager;
|
||||
import org.alfresco.transform.base.fs.FileManager;
|
||||
import org.alfresco.transform.base.util.OutputStreamLengthRecorder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* Manages the input and output streams and any temporary files that have been created.
|
||||
*/
|
||||
@Component
|
||||
public class TransformManagerImpl implements TransformManager
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(TransformManagerImpl.class);
|
||||
|
||||
private HttpServletRequest request;
|
||||
private ProcessHandler processHandler;
|
||||
private InputStream inputStream;
|
||||
private OutputStreamLengthRecorder outputStreamLengthRecorder;
|
||||
private String sourceMimetype;
|
||||
private String targetMimetype;
|
||||
private File sourceFile;
|
||||
private File targetFile;
|
||||
private boolean keepTargetFile;
|
||||
private boolean createSourceFileCalled;
|
||||
private boolean createTargetFileCalled;
|
||||
private Boolean startedWithSourceFile;
|
||||
private Boolean startedWithTargetFile;
|
||||
|
||||
public void setRequest(HttpServletRequest request)
|
||||
{
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
public void setProcessHandler(ProcessHandler processHandler)
|
||||
{
|
||||
this.processHandler = processHandler;
|
||||
}
|
||||
|
||||
@Override public String getRequestId()
|
||||
{
|
||||
return processHandler.getReference();
|
||||
}
|
||||
|
||||
public InputStream setInputStream(InputStream inputStream)
|
||||
{
|
||||
this.inputStream = inputStream;
|
||||
if (startedWithSourceFile == null)
|
||||
{
|
||||
startedWithSourceFile = false;
|
||||
}
|
||||
return inputStream;
|
||||
}
|
||||
|
||||
public OutputStream getOutputStream()
|
||||
{
|
||||
return outputStreamLengthRecorder;
|
||||
}
|
||||
|
||||
public OutputStream setOutputStream(OutputStream outputStream)
|
||||
{
|
||||
outputStreamLengthRecorder = new OutputStreamLengthRecorder(outputStream);
|
||||
if (startedWithTargetFile == null)
|
||||
{
|
||||
startedWithTargetFile = false;
|
||||
}
|
||||
return outputStreamLengthRecorder;
|
||||
}
|
||||
|
||||
public Long getOutputLength()
|
||||
{
|
||||
return outputStreamLengthRecorder.getLength();
|
||||
}
|
||||
|
||||
public void setSourceMimetype(String sourceMimetype)
|
||||
{
|
||||
this.sourceMimetype = sourceMimetype;
|
||||
}
|
||||
|
||||
public void setTargetMimetype(String targetMimetype)
|
||||
{
|
||||
this.targetMimetype = targetMimetype;
|
||||
}
|
||||
|
||||
public File getSourceFile()
|
||||
{
|
||||
return sourceFile;
|
||||
}
|
||||
|
||||
public void setSourceFile(File sourceFile)
|
||||
{
|
||||
this.sourceFile = sourceFile;
|
||||
if (startedWithSourceFile == null)
|
||||
{
|
||||
startedWithSourceFile = true;
|
||||
}
|
||||
}
|
||||
|
||||
public File getTargetFile()
|
||||
{
|
||||
return targetFile;
|
||||
}
|
||||
|
||||
public void setTargetFile(File targetFile)
|
||||
{
|
||||
this.targetFile = targetFile;
|
||||
if (startedWithTargetFile == null)
|
||||
{
|
||||
startedWithTargetFile = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void keepTargetFile()
|
||||
{
|
||||
keepTargetFile = true;
|
||||
}
|
||||
|
||||
@Override public File createSourceFile()
|
||||
{
|
||||
if (createSourceFileCalled)
|
||||
{
|
||||
throw new IllegalStateException("createSourceFile has already been called");
|
||||
}
|
||||
createSourceFileCalled = true;
|
||||
|
||||
if (sourceFile == null)
|
||||
{
|
||||
sourceFile = FileManager.createSourceFile(request, inputStream, sourceMimetype);
|
||||
}
|
||||
return sourceFile;
|
||||
}
|
||||
|
||||
@Override public File createTargetFile()
|
||||
{
|
||||
if (createTargetFileCalled)
|
||||
{
|
||||
throw new IllegalStateException("createTargetFile has already been called");
|
||||
}
|
||||
createTargetFileCalled = true;
|
||||
|
||||
if (targetFile == null)
|
||||
{
|
||||
targetFile = FileManager.createTargetFile(request, sourceMimetype, targetMimetype);
|
||||
}
|
||||
return targetFile;
|
||||
}
|
||||
|
||||
public void copyTargetFileToOutputStream() throws IOException
|
||||
{
|
||||
if (targetFile != null)
|
||||
{
|
||||
if (!startedWithTargetFile)
|
||||
{
|
||||
FileManager.copyFileToOutputStream(targetFile, outputStreamLengthRecorder);
|
||||
}
|
||||
else if (createTargetFileCalled)
|
||||
{
|
||||
outputStreamLengthRecorder.setByteCount(targetFile.length());
|
||||
}
|
||||
else
|
||||
{
|
||||
outputStreamLengthRecorder.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteSourceFile()
|
||||
{
|
||||
if (sourceFile != null && !sourceFile.delete())
|
||||
{
|
||||
logger.error("Failed to delete temporary source file {}", sourceFile.getPath());
|
||||
}
|
||||
outputStreamLengthRecorder = null;
|
||||
sourceFile = null;
|
||||
createSourceFileCalled = false;
|
||||
startedWithSourceFile = null;
|
||||
}
|
||||
|
||||
public void deleteTargetFile()
|
||||
{
|
||||
if (!keepTargetFile && targetFile != null && !targetFile.delete())
|
||||
{
|
||||
logger.error("Failed to delete temporary target file {}", targetFile.getPath());
|
||||
}
|
||||
targetFile = null;
|
||||
createTargetFileCalled = false;
|
||||
startedWithTargetFile = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream respondWithFragment(Integer index, boolean finished) throws IOException
|
||||
{
|
||||
if (request != null)
|
||||
{
|
||||
throw new IllegalStateException("Fragments may only be sent via message queues. This an http request");
|
||||
}
|
||||
|
||||
return processHandler.respondWithFragment(index, finished);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.util;
|
||||
|
||||
import org.alfresco.transform.base.CustomTransformer;
|
||||
import org.alfresco.transform.base.TransformManager;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Helper interface for older code that uses Files rather than InputStreams and OutputStreams.
|
||||
* If you can, refactor your code to NOT use Files.
|
||||
*/
|
||||
public interface CustomTransformerFileAdaptor extends CustomTransformer
|
||||
{
|
||||
@Override
|
||||
default void transform(String sourceMimetype, InputStream inputStream,
|
||||
String targetMimetype, OutputStream outputStream,
|
||||
Map<String, String> transformOptions, TransformManager transformManager) throws Exception
|
||||
{
|
||||
File sourceFile = transformManager.createSourceFile();
|
||||
File targetFile = transformManager.createTargetFile();
|
||||
transform(sourceMimetype, targetMimetype, transformOptions, sourceFile, targetFile, transformManager);
|
||||
}
|
||||
|
||||
void transform(String sourceMimetype, String targetMimetype, Map<String, String> transformOptions,
|
||||
File sourceFile, File targetFile, TransformManager transformManager) throws Exception;
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.util;
|
||||
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class OutputStreamLengthRecorder extends FilterOutputStream
|
||||
{
|
||||
private long byteCount;
|
||||
|
||||
public OutputStreamLengthRecorder(OutputStream outputStream)
|
||||
{
|
||||
super(outputStream);
|
||||
}
|
||||
|
||||
public long getLength()
|
||||
{
|
||||
return byteCount;
|
||||
}
|
||||
|
||||
public void setByteCount(long byteCount)
|
||||
{
|
||||
this.byteCount = byteCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException
|
||||
{
|
||||
super.write(b);
|
||||
byteCount++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte b[], int off, int len) throws IOException
|
||||
{
|
||||
super.write(b, off, len);
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.util;
|
||||
|
||||
public class Util
|
||||
{
|
||||
private Util()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely converts a {@link String} to an {@link Integer}
|
||||
*
|
||||
* @param param String to be converted
|
||||
* @return Null if param is null or converted value as {@link Integer}
|
||||
*/
|
||||
public static Integer stringToInteger(final String param)
|
||||
{
|
||||
return param == null ? null : Integer.parseInt(param);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely converts a {@link String} to a {@link Boolean}
|
||||
*
|
||||
* @param param String to be converted
|
||||
* @return Null if param is null or converted value as {@link Boolean}
|
||||
*/
|
||||
public static Boolean stringToBoolean(final String param)
|
||||
{
|
||||
return param == null ? null : Boolean.parseBoolean(param);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely converts a {@link String} to a {@link Long}
|
||||
*
|
||||
* @param param String to be converted
|
||||
* @return Null if param is null or converted value as {@link Boolean}
|
||||
*/
|
||||
public static Long stringToLong(final String param)
|
||||
{
|
||||
return param == null ? null : Long.parseLong(param);
|
||||
}
|
||||
}
|
||||
64
engines/base/src/main/resources/application.yaml
Normal file
64
engines/base/src/main/resources/application.yaml
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
spring:
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 8192MB
|
||||
max-request-size: 8192MB
|
||||
activemq:
|
||||
broker-url: ${ACTIVEMQ_URL:nio://localhost:61616}?jms.watchTopicAdvisories=false
|
||||
user: ${ACTIVEMQ_USER:admin}
|
||||
password: ${ACTIVEMQ_PASSWORD:admin}
|
||||
pool:
|
||||
enabled: true
|
||||
max-connections: 20
|
||||
jackson:
|
||||
default-property-inclusion: non_empty
|
||||
|
||||
activemq:
|
||||
url: ${ACTIVEMQ_URL:false}
|
||||
|
||||
server:
|
||||
port: ${SERVER_PORT:8090}
|
||||
error:
|
||||
include-message: ALWAYS
|
||||
|
||||
# Historic values (AVOID) - Exist to help with transition to newer versions
|
||||
transformer-routes-path: ${TRANSFORMER_ROUTES_FILE_LOCATION:transformer-pipelines.json}
|
||||
|
||||
logging:
|
||||
level:
|
||||
org.alfresco.transform.common.TransformerDebug: debug
|
||||
|
||||
filestore-url: ${FILE_STORE_URL:http://localhost:8099/alfresco/api/-default-/private/sfs/versions/1/file}
|
||||
|
||||
transform:
|
||||
core:
|
||||
version: @project.version@
|
||||
engine:
|
||||
config:
|
||||
cron: 0 0 * * * * # once an hour on the hour
|
||||
retry:
|
||||
attempts: 10
|
||||
timeout: 10 # seconds
|
||||
|
||||
jms-listener:
|
||||
concurrency: ${JMS_LISTENER_CONCURRENCY:1-10}
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include:
|
||||
- metrics
|
||||
- prometheus
|
||||
- health
|
||||
metrics:
|
||||
enable[http]: false
|
||||
enable[logback]: false
|
||||
enable[tomcat]: false
|
||||
enable[jvm.classes]: false
|
||||
|
||||
container:
|
||||
name: ${HOSTNAME:t-engine}
|
||||
isTRouter: false
|
||||
behind-ingres: false
|
||||
27
engines/base/src/main/resources/templates/error.html
Normal file
27
engines/base/src/main/resources/templates/error.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" xml:lang="en">
|
||||
<head>
|
||||
<title th:text="${title}"/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div>
|
||||
<h2 th:text="${title}"></h2>
|
||||
<div th:if="${message}">
|
||||
<h3 th:text="${message}"></h3>
|
||||
</div>
|
||||
<p th:text="${status} + ' - ' + ${error}"></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<br/>
|
||||
<a th:href="${proxyPathPrefix+'/'}">Test</a>
|
||||
<a th:href="${proxyPathPrefix+'/log'}">Log</a>
|
||||
<a th:href="${proxyPathPrefix+'/ready'}">Ready</a>
|
||||
<a th:href="${proxyPathPrefix+'/live'}">Live</a>
|
||||
<a th:href="${proxyPathPrefix+'/transform/config?configVersion=9999'}">Config</a>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
51
engines/base/src/main/resources/templates/log.html
Normal file
51
engines/base/src/main/resources/templates/log.html
Normal file
@@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" xml:lang="en">
|
||||
<head>
|
||||
<title th:text="${title}"/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div>
|
||||
<h2 th:text="${title}"></h2>
|
||||
<div th:if="${log}">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Time</th>
|
||||
<th>Status Code</th>
|
||||
<th>Duration (ms)</th>
|
||||
<th>Source</th>
|
||||
<th></th>
|
||||
<th>Target</th>
|
||||
<th></th>
|
||||
<th>Options</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
<tr th:each="entry : ${log}">
|
||||
<td th:text="${entry.id}"></td>
|
||||
<td th:text="${#dates.format(entry.date, 'HH:mm:ss')}"></td>
|
||||
<td th:text="${entry.statusCode}"></td>
|
||||
<td th:text="${entry.duration}"></td>
|
||||
<td th:text="${entry.source}"></td>
|
||||
<td th:text="${entry.sourceSize}"></td>
|
||||
<td th:text="${entry.target}"></td>
|
||||
<td th:text="${entry.targetSize}"></td>
|
||||
<td th:text="${entry.options}"></td>
|
||||
<td th:text="${entry.message}"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<br/>
|
||||
<a th:href="${proxyPathPrefix+'/'}">Test</a>
|
||||
Log
|
||||
<a th:href="${proxyPathPrefix+'/ready'}">Ready</a>
|
||||
<a th:href="${proxyPathPrefix+'/live'}">Live</a>
|
||||
<a th:href="${proxyPathPrefix+'/transform/config?configVersion=9999'}">Config</a>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
63
engines/base/src/main/resources/templates/test.html
Normal file
63
engines/base/src/main/resources/templates/test.html
Normal file
@@ -0,0 +1,63 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" xml:lang="en">
|
||||
<head>
|
||||
<title th:text="${title}"/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<h2 th:text="${title}"></h2>
|
||||
<form method="POST" enctype="multipart/form-data" th:action="${proxyPathPrefix+'/test'}">
|
||||
<table>
|
||||
<tr><td><div style="text-align:right">file</div></td><td><input type="file" name="file" /></td></tr>
|
||||
<tr><td><div style="text-align:right">directAccessUrl</div></td><td><input type="text" name="directAccessUrl"/></td></tr>
|
||||
<tr><td><div style="text-align:right">sourceMimetype</div></td><td><input type="text" name="sourceMimetype" value="" /></td>
|
||||
<td><select name="_sourceMimetype">
|
||||
<option value="" >-- by file extension --</option>
|
||||
<option value="image/jpeg" >jpg</option>
|
||||
<option value="image/png">png</option>
|
||||
<option value="application/pdf">pdf</option>
|
||||
<option value="application/vnd.openxmlformats-officedocument.wordprocessingml.document">docx</option>
|
||||
<option value="application/vnd.openxmlformats-officedocument.presentationml.slideshow">ppsx</option>
|
||||
<option value="text/html">html</option>
|
||||
<option value="text/plain">txt</option>
|
||||
</select></td></tr>
|
||||
<tr><td><div style="text-align:right">targetMimetype</div></td><td><input type="text" name="targetMimetype" value="" /></td>
|
||||
<td><select name="_targetMimetype">
|
||||
<option value="" >-- by file extension --</option>
|
||||
<option value="image/jpeg" >jpg</option>
|
||||
<option value="image/png">png</option>
|
||||
<option value="application/pdf">pdf</option>
|
||||
<option value="application/vnd.openxmlformats-officedocument.wordprocessingml.document">docx</option>
|
||||
<option value="application/vnd.openxmlformats-officedocument.presentationml.slideshow">ppsx</option>
|
||||
<option value="text/html">html</option>
|
||||
<option value="text/plain">txt</option>
|
||||
</select></td></tr>
|
||||
|
||||
<th:block th:each="i: ${#numbers.sequence(0, T(java.lang.Math).min(18, transformOptions.size) - 1)}">
|
||||
<tr><td><select th:name="${'name'+i}">
|
||||
<option th:each="transformOption, iStat: ${transformOptions}"
|
||||
th:value="${transformOption}" th:text="${transformOption}"
|
||||
th:selected="${iStat.index eq i}"/>
|
||||
</select></td><td><input type="text" th:name="${'value'+i}" /></td></tr>
|
||||
</th:block>
|
||||
|
||||
<th:block th:each="i: ${#numbers.sequence(T(java.lang.Math).min(18, transformOptions.size), T(java.lang.Math).min(18, transformOptions.size) + 2)}">
|
||||
<tr><td><input type="text" th:name="${'name'+i}" value="" /></td>
|
||||
<td><input type="text" th:name="${'value'+i}" value="" /></td></tr>
|
||||
</th:block>
|
||||
|
||||
<tr><td><div style="text-align:right">timeout</div></td><td><input type="text" name="timeout" value="" /></td></tr>
|
||||
<tr><td></td><td><input type="submit" value="Transform" /></td></tr>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
Test
|
||||
<a th:href="${proxyPathPrefix+'/log'}">Log</a>
|
||||
<a th:href="${proxyPathPrefix+'/ready'}">Ready</a>
|
||||
<a th:href="${proxyPathPrefix+'/live'}">Live</a>
|
||||
<a th:href="${proxyPathPrefix+'/transform/config?configVersion=9999'}">Config</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,451 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.alfresco.transform.base.sfs.SharedFileStoreClient;
|
||||
import org.alfresco.transform.base.executors.CommandExecutor;
|
||||
import org.alfresco.transform.base.executors.RuntimeExec;
|
||||
import org.alfresco.transform.base.model.FileRefEntity;
|
||||
import org.alfresco.transform.base.model.FileRefResponse;
|
||||
import org.alfresco.transform.base.probes.ProbeTransform;
|
||||
import org.alfresco.transform.base.transform.TransformHandler;
|
||||
import org.alfresco.transform.client.model.TransformReply;
|
||||
import org.alfresco.transform.client.model.TransformRequest;
|
||||
import org.alfresco.transform.registry.TransformServiceRegistry;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.boot.test.mock.mockito.SpyBean;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.ResultActions;
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
|
||||
import static org.alfresco.transform.common.RequestParamMap.DIRECT_ACCESS_URL;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_TRANSFORM;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.http.HttpHeaders.ACCEPT;
|
||||
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
|
||||
import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
||||
import static org.springframework.http.HttpStatus.CREATED;
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Super class for unit testing.
|
||||
*/
|
||||
@SpringBootTest(classes={org.alfresco.transform.base.Application.class})
|
||||
@AutoConfigureMockMvc
|
||||
public abstract class AbstractBaseTest
|
||||
{
|
||||
// Added as part of ATS-702 to allow test resources to be read from the imported jar files to prevent test
|
||||
// resource duplication
|
||||
@TempDir
|
||||
public File tempDir;
|
||||
|
||||
@Autowired
|
||||
protected TransformHandler transformHandler;
|
||||
@Autowired
|
||||
protected TransformController controller;
|
||||
|
||||
@Autowired
|
||||
protected MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
protected ObjectMapper objectMapper;
|
||||
|
||||
@MockBean
|
||||
protected SharedFileStoreClient sharedFileStoreClient;
|
||||
|
||||
@SpyBean
|
||||
protected TransformServiceRegistry transformRegistry;
|
||||
|
||||
protected String sourceExtension;
|
||||
protected String targetExtension;
|
||||
protected String sourceMimetype;
|
||||
protected String targetMimetype;
|
||||
protected HashMap<String, String> options = new HashMap<>();
|
||||
|
||||
protected MockMultipartFile sourceFile;
|
||||
protected String expectedOptions;
|
||||
protected String expectedSourceSuffix;
|
||||
protected Long expectedTimeout = 0L;
|
||||
protected byte[] sourceFileBytes;
|
||||
|
||||
/**
|
||||
* The expected result. Taken resting target quick file's bytes.
|
||||
*
|
||||
* Note: These checks generally don't work on Windows (Mac and Linux are okay). Possibly to do with byte order
|
||||
* loading.
|
||||
*/
|
||||
protected byte[] expectedTargetFileBytes;
|
||||
|
||||
// Called by sub class
|
||||
private CommandExecutor commandExecutor;
|
||||
private RuntimeExec origTransformCommand;
|
||||
private RuntimeExec origCheckCommand;
|
||||
|
||||
protected void setMockExternalCommandsOnTransformer(CommandExecutor commandExecutor, RuntimeExec mockTransformCommand,
|
||||
RuntimeExec mockCheckCommand)
|
||||
{
|
||||
this.commandExecutor = commandExecutor;
|
||||
origTransformCommand = (RuntimeExec) ReflectionTestUtils.getField(commandExecutor, "transformCommand");
|
||||
origCheckCommand = (RuntimeExec) ReflectionTestUtils.getField(commandExecutor, "transformCommand");
|
||||
ReflectionTestUtils.setField(commandExecutor, "transformCommand", mockTransformCommand);
|
||||
ReflectionTestUtils.setField(commandExecutor, "checkCommand", mockCheckCommand);
|
||||
}
|
||||
|
||||
protected void resetExternalCommandsOnTransformer()
|
||||
{
|
||||
ReflectionTestUtils.setField(commandExecutor, "transformCommand", origTransformCommand);
|
||||
ReflectionTestUtils.setField(commandExecutor, "checkCommand", origCheckCommand);
|
||||
}
|
||||
|
||||
protected void mockTransformCommand(String sourceExtension,
|
||||
String targetExtension, String sourceMimetype,
|
||||
boolean readTargetFileBytes) throws IOException
|
||||
{
|
||||
}
|
||||
|
||||
protected void updateTransformRequestWithSpecificOptions(TransformRequest transformRequest)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* This method ends up being the core of the mock.
|
||||
* It copies content from an existing file in the resources folder to the desired location
|
||||
* in order to simulate a successful transformation.
|
||||
*
|
||||
* @param actualTargetExtension Requested extension.
|
||||
* @param testFile The test file (transformed) - basically the result.
|
||||
* @param targetFile The location where the content from the testFile should be copied
|
||||
* @throws IOException in case of any errors.
|
||||
*/
|
||||
public void generateTargetFileFromResourceFile(String actualTargetExtension, File testFile,
|
||||
File targetFile) throws IOException
|
||||
{
|
||||
if (testFile == null)
|
||||
{
|
||||
testFile = getTestFile("quick." + actualTargetExtension, false);
|
||||
}
|
||||
if (testFile != null)
|
||||
{
|
||||
try (var inputStream = new FileInputStream(testFile);
|
||||
var outputStream = new FileOutputStream(targetFile))
|
||||
{
|
||||
FileChannel source = inputStream.getChannel();
|
||||
FileChannel target = outputStream.getChannel();
|
||||
target.transferFrom(source, 0, source.size());
|
||||
|
||||
} catch (Exception e)
|
||||
{
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected byte[] readTestFile(String extension) throws IOException
|
||||
{
|
||||
return Files.readAllBytes(getTestFile("quick." + extension, true).toPath());
|
||||
}
|
||||
|
||||
protected File getTestFile(String testFilename, boolean required) throws IOException
|
||||
{
|
||||
return getTestFile(testFilename, required, tempDir);
|
||||
}
|
||||
|
||||
public static File getTestFile(String testFilename, boolean required, File tempDir) throws IOException
|
||||
{
|
||||
File testFile = null;
|
||||
ClassLoader classLoader = AbstractBaseTest.class.getClassLoader();
|
||||
URL testFileUrl = classLoader.getResource(testFilename);
|
||||
if (required && testFileUrl == null)
|
||||
{
|
||||
throw new IOException("The test file " + testFilename +
|
||||
" does not exist in the resources directory");
|
||||
}
|
||||
// Added as part of ATS-702 to allow test resources to be read from the imported jar files to prevent test
|
||||
// resource duplication
|
||||
if (testFileUrl != null)
|
||||
{
|
||||
// Each use of the tempDir should result in a unique directory being used
|
||||
testFile = new File(tempDir, testFilename);
|
||||
Files.copy(classLoader.getResourceAsStream(testFilename), testFile.toPath(),REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
return testFileUrl == null ? null : testFile;
|
||||
}
|
||||
|
||||
protected MockHttpServletRequestBuilder mockMvcRequest(String url, MockMultipartFile sourceFile, String... params)
|
||||
{
|
||||
if (sourceFile == null)
|
||||
{
|
||||
return mockMvcRequestWithoutMockMultipartFile(url, params);
|
||||
}
|
||||
else
|
||||
{
|
||||
return mockMvcRequestWithMockMultipartFile(url, sourceFile, params);
|
||||
}
|
||||
}
|
||||
|
||||
private MockHttpServletRequestBuilder mockMvcRequestWithoutMockMultipartFile(String url, String... params)
|
||||
{
|
||||
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.multipart(url);
|
||||
|
||||
if (params.length % 2 != 0)
|
||||
{
|
||||
throw new IllegalArgumentException("each param should have a name and value.");
|
||||
}
|
||||
for (int i = 0; i < params.length; i += 2)
|
||||
{
|
||||
builder = builder.param(params[i], params[i + 1]);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private MockHttpServletRequestBuilder mockMvcRequestWithMockMultipartFile(String url, MockMultipartFile sourceFile,
|
||||
String... params)
|
||||
{
|
||||
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.multipart(ENDPOINT_TRANSFORM).file(sourceFile);
|
||||
|
||||
if (params.length % 2 != 0)
|
||||
{
|
||||
throw new IllegalArgumentException("each param should have a name and value.");
|
||||
}
|
||||
for (int i = 0; i < params.length; i += 2)
|
||||
{
|
||||
builder = builder.param(params[i], params[i + 1]);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
protected TransformRequest createTransformRequest(String sourceFileRef, File sourceFile)
|
||||
{
|
||||
return TransformRequest.builder()
|
||||
.withRequestId("1")
|
||||
.withSchema(1)
|
||||
.withClientData("Alfresco Digital Business Platform")
|
||||
.withTransformRequestOptions(options)
|
||||
.withSourceReference(sourceFileRef)
|
||||
.withSourceExtension(sourceExtension)
|
||||
.withSourceMediaType(sourceMimetype)
|
||||
.withSourceSize(sourceFile.length())
|
||||
.withTargetExtension(targetExtension)
|
||||
.withTargetMediaType(targetMimetype)
|
||||
.withInternalContextForTransformEngineTests()
|
||||
.build();
|
||||
}
|
||||
|
||||
public static void resetProbeForTesting(ProbeTransform probe)
|
||||
{
|
||||
ReflectionTestUtils.setField(probe, "probeCount", 0);
|
||||
ReflectionTestUtils.setField(probe, "transCount", 0);
|
||||
ReflectionTestUtils.setField(probe, "normalTime", 0);
|
||||
ReflectionTestUtils.setField(probe, "maxTime", Long.MAX_VALUE);
|
||||
ReflectionTestUtils.setField(probe, "nextTransformTime", 0);
|
||||
|
||||
((AtomicBoolean)ReflectionTestUtils.getField(probe, "initialised")).set(false);
|
||||
((AtomicBoolean)ReflectionTestUtils.getField(probe, "readySent")).set(false);
|
||||
((AtomicLong)ReflectionTestUtils.getField(probe, "transformCount")).set(0);
|
||||
((AtomicBoolean)ReflectionTestUtils.getField(probe, "die")).set(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleTransformTest() throws Exception
|
||||
{
|
||||
mockMvc.perform(
|
||||
mockMvcRequest(ENDPOINT_TRANSFORM, sourceFile))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().bytes(expectedTargetFileBytes))
|
||||
.andExpect(header().string("Content-Disposition",
|
||||
"attachment; filename*=UTF-8''transform." + targetExtension));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dotDotSourceFilenameTest() throws Exception
|
||||
{
|
||||
sourceFile = new MockMultipartFile("file", "../quick." + sourceExtension, sourceMimetype, sourceFileBytes);
|
||||
|
||||
mockMvc.perform(
|
||||
mockMvcRequest(ENDPOINT_TRANSFORM, sourceFile))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().bytes(expectedTargetFileBytes))
|
||||
.andExpect(header().string("Content-Disposition",
|
||||
"attachment; filename*=UTF-8''transform." + targetExtension));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noExtensionSourceFilenameTest() throws Exception
|
||||
{
|
||||
sourceFile = new MockMultipartFile("file", "../quick", sourceMimetype, sourceFileBytes);
|
||||
|
||||
mockMvc.perform(
|
||||
mockMvcRequest(ENDPOINT_TRANSFORM, sourceFile))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().bytes(expectedTargetFileBytes))
|
||||
.andExpect(header().string("Content-Disposition",
|
||||
"attachment; filename*=UTF-8''transform." + targetExtension));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateMaxTime() throws Exception
|
||||
{
|
||||
ProbeTransform probeTransform = controller.getProbeTransform();
|
||||
resetProbeForTesting(probeTransform);
|
||||
probeTransform.setLivenessPercent(110);
|
||||
|
||||
long[][] values = new long[][]{
|
||||
{5000, 0, Long.MAX_VALUE}, // 1st transform is ignored
|
||||
{1000, 1000, 2100}, // 1000 + 1000*1.1
|
||||
{3000, 2000, 4200}, // 2000 + 2000*1.1
|
||||
{2000, 2000, 4200},
|
||||
{6000, 3000, 6300},
|
||||
{8000, 4000, 8400},
|
||||
{4444, 4000, 8400}, // no longer in the first few, so normal and max times don't change
|
||||
{5555, 4000, 8400}
|
||||
};
|
||||
|
||||
for (long[] v : values)
|
||||
{
|
||||
long time = v[0];
|
||||
long expectedNormalTime = v[1];
|
||||
long expectedMaxTime = v[2];
|
||||
|
||||
probeTransform.calculateMaxTime(time, true);
|
||||
assertEquals(expectedNormalTime, probeTransform.getNormalTime());
|
||||
assertEquals(expectedMaxTime, probeTransform.getMaxTime());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyPojoTransform() throws Exception
|
||||
{
|
||||
// Transformation Request POJO
|
||||
TransformRequest transformRequest = new TransformRequest();
|
||||
|
||||
// Serialize and call the transformer
|
||||
String tr = objectMapper.writeValueAsString(transformRequest);
|
||||
String transformationReplyAsString = mockMvc
|
||||
.perform(MockMvcRequestBuilders
|
||||
.post(ENDPOINT_TRANSFORM)
|
||||
.header(ACCEPT, APPLICATION_JSON_VALUE)
|
||||
.header(CONTENT_TYPE, APPLICATION_JSON_VALUE)
|
||||
.content(tr))
|
||||
.andExpect(status().is(BAD_REQUEST.value()))
|
||||
.andReturn().getResponse().getContentAsString();
|
||||
|
||||
TransformReply transformReply = objectMapper.readValue(transformationReplyAsString,
|
||||
TransformReply.class);
|
||||
|
||||
// Assert the reply
|
||||
assertEquals(BAD_REQUEST.value(), transformReply.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queueTransformRequestUsingDirectAccessUrlTest() throws Exception
|
||||
{
|
||||
// Files
|
||||
String sourceFileRef = UUID.randomUUID().toString();
|
||||
File sourceFile = getTestFile("quick." + sourceExtension, true);
|
||||
String targetFileRef = UUID.randomUUID().toString();
|
||||
|
||||
TransformRequest transformRequest = createTransformRequest(sourceFileRef, sourceFile);
|
||||
Map<String, String> transformRequestOptions = transformRequest.getTransformRequestOptions();
|
||||
|
||||
String directUrl = "file://" + sourceFile.toPath();
|
||||
|
||||
transformRequestOptions.put(DIRECT_ACCESS_URL, directUrl);
|
||||
|
||||
when(sharedFileStoreClient.saveFile(any()))
|
||||
.thenReturn(new FileRefResponse(new FileRefEntity(targetFileRef)));
|
||||
|
||||
// Update the Transformation Request with any specific params before sending it
|
||||
updateTransformRequestWithSpecificOptions(transformRequest);
|
||||
|
||||
// Serialize and call the transformer
|
||||
String tr = objectMapper.writeValueAsString(transformRequest);
|
||||
String transformationReplyAsString = mockMvc
|
||||
.perform(MockMvcRequestBuilders
|
||||
.post(ENDPOINT_TRANSFORM)
|
||||
.header(ACCEPT, APPLICATION_JSON_VALUE)
|
||||
.header(CONTENT_TYPE, APPLICATION_JSON_VALUE)
|
||||
.content(tr))
|
||||
.andExpect(status().is(CREATED.value()))
|
||||
.andReturn().getResponse().getContentAsString();
|
||||
TransformReply transformReply = objectMapper.readValue(transformationReplyAsString, TransformReply.class);
|
||||
|
||||
// Assert the reply
|
||||
assertEquals(transformRequest.getRequestId(), transformReply.getRequestId());
|
||||
assertEquals(transformRequest.getClientData(), transformReply.getClientData());
|
||||
assertEquals(transformRequest.getSchema(), transformReply.getSchema());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void httpTransformRequestUsingDirectAccessUrlTest() throws Exception
|
||||
{
|
||||
File dauSourceFile = getTestFile("quick." + sourceExtension, true);
|
||||
String directUrl = "file://" + dauSourceFile.toPath();
|
||||
|
||||
ResultActions resultActions = mockMvc.perform(
|
||||
mockMvcRequest(ENDPOINT_TRANSFORM, null)
|
||||
.param(DIRECT_ACCESS_URL, directUrl))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string("Content-Disposition",
|
||||
"attachment; filename*=UTF-8''transform."+targetExtension));
|
||||
|
||||
if (expectedTargetFileBytes != null)
|
||||
{
|
||||
resultActions.andExpect(content().bytes(expectedTargetFileBytes));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.alfresco.transform.base.fakes.FakeTransformEngineWithAllInOne;
|
||||
import org.alfresco.transform.base.fakes.FakeTransformEngineWithOneCustomTransformer;
|
||||
import org.alfresco.transform.base.fakes.FakeTransformEngineWithTwoCustomTransformers;
|
||||
import org.alfresco.transform.base.fakes.FakeTransformerPdf2Jpg;
|
||||
import org.alfresco.transform.base.fakes.FakeTransformerPdf2Png;
|
||||
import org.alfresco.transform.base.fakes.FakeTransformerTxT2Pdf;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
import static org.alfresco.transform.base.TransformControllerTest.assertConfig;
|
||||
import static org.alfresco.transform.base.TransformControllerTest.getLogMessagesFor;
|
||||
import static org.alfresco.transform.base.TransformControllerTest.resetProbeForTesting;
|
||||
import static org.alfresco.transform.common.Mimetype.MIMETYPE_IMAGE_JPEG;
|
||||
import static org.alfresco.transform.common.Mimetype.MIMETYPE_PDF;
|
||||
import static org.alfresco.transform.common.Mimetype.MIMETYPE_TEXT_PLAIN;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_ERROR;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_LIVE;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_LOG;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_READY;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_ROOT;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_TRANSFORM;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_TRANSFORM_CONFIG;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_TRANSFORM_CONFIG_LATEST;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_VERSION;
|
||||
import static org.alfresco.transform.common.RequestParamMap.PAGE_REQUEST_PARAM;
|
||||
import static org.alfresco.transform.common.RequestParamMap.SOURCE_MIMETYPE;
|
||||
import static org.alfresco.transform.common.RequestParamMap.TARGET_MIMETYPE;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Testing TransformController functionality where there are multiple TransformEngines brought together
|
||||
* in a single t-engine.
|
||||
*
|
||||
* Repeats a set of tests from {@link TransformControllerTest}, which tests the single TransformEngine case.
|
||||
*/
|
||||
@AutoConfigureMockMvc
|
||||
@SpringBootTest(classes={org.alfresco.transform.base.Application.class})
|
||||
@ContextConfiguration(classes = {
|
||||
FakeTransformEngineWithAllInOne.class,
|
||||
FakeTransformEngineWithTwoCustomTransformers.class,
|
||||
FakeTransformerTxT2Pdf.class,
|
||||
FakeTransformerPdf2Png.class,
|
||||
FakeTransformEngineWithOneCustomTransformer.class,
|
||||
FakeTransformerPdf2Jpg.class})
|
||||
public class TransformControllerAllInOneTest
|
||||
{
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
@Autowired
|
||||
private TransformController transformController;
|
||||
@Autowired
|
||||
protected ObjectMapper objectMapper;
|
||||
@Autowired
|
||||
private String coreVersion;
|
||||
|
||||
@Test
|
||||
public void testInitEngine()
|
||||
{
|
||||
assertEquals(FakeTransformEngineWithAllInOne.class.getSimpleName(),
|
||||
transformController.transformEngine.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStartupLogsIncludeEngineMessages()
|
||||
{
|
||||
StringJoiner controllerLogMessages = getLogMessagesFor(TransformController.class);
|
||||
|
||||
transformController.startup();
|
||||
|
||||
assertEquals(
|
||||
"--------------------------------------------------------------------------------------------------------------------------------------------------------------\n"
|
||||
+ "Startup 0000 AllInOne\n"
|
||||
+ "Line 2 0000 AllInOne\n"
|
||||
+ "Line 3\n"
|
||||
+ "--------------------------------------------------------------------------------------------------------------------------------------------------------------\n"
|
||||
+ "Starting application components... Done",
|
||||
controllerLogMessages.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVersionEndpointIncludesAvailable() throws Exception
|
||||
{
|
||||
mockMvc.perform(MockMvcRequestBuilders.get(ENDPOINT_VERSION))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string("AllInOne "+coreVersion));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRootEndpointReturnsTestPage() throws Exception
|
||||
{
|
||||
mockMvc.perform(MockMvcRequestBuilders.get(ENDPOINT_ROOT))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(containsString("AllInOne Test Page")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testErrorEndpointReturnsErrorPage() throws Exception
|
||||
{
|
||||
mockMvc.perform(MockMvcRequestBuilders.get(ENDPOINT_ERROR))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(containsString("AllInOne Error Page")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLogEndpointReturnsLogPage() throws Exception
|
||||
{
|
||||
mockMvc.perform(MockMvcRequestBuilders.get(ENDPOINT_LOG))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(containsString("AllInOne Log Entries")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadyEndpointReturnsSuccessful() throws Exception
|
||||
{
|
||||
resetProbeForTesting(transformController);
|
||||
mockMvc.perform(MockMvcRequestBuilders.get(ENDPOINT_READY))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(containsString("Success - ")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLiveEndpointReturnsSuccessful() throws Exception
|
||||
{
|
||||
resetProbeForTesting(transformController);
|
||||
mockMvc.perform(MockMvcRequestBuilders.get(ENDPOINT_LIVE))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(containsString("Success - ")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConfigEndpointReturnsOriginalConfigFormat() throws Exception
|
||||
{
|
||||
// The transformer's options should not include directAccessUrl as this is the default version of config
|
||||
assertConfig(ENDPOINT_TRANSFORM_CONFIG,
|
||||
"Pdf2Jpg,null,imageOptions\n"
|
||||
+ "Pdf2Png,null,imageOptions\n"
|
||||
+ "TxT2Pdf,null,docOptions\n"
|
||||
+ "Txt2JpgViaPdf,null,imageOptions\n"
|
||||
+ "Txt2PngViaPdf,null,imageOptions",
|
||||
"docOptions,imageOptions", mockMvc, objectMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConfigLatestEndpointReturnsCoreVersionAndDirectAccessUrlOption() throws Exception
|
||||
{
|
||||
assertConfig(ENDPOINT_TRANSFORM_CONFIG_LATEST,
|
||||
"Pdf2Jpg,"+coreVersion+",directAccessUrl,imageOptions\n"
|
||||
+ "Pdf2Png,"+coreVersion+",directAccessUrl,imageOptions\n"
|
||||
+ "TxT2Pdf,"+coreVersion+",directAccessUrl,docOptions\n"
|
||||
+ "Txt2JpgViaPdf,"+coreVersion+",directAccessUrl,imageOptions\n"
|
||||
+ "Txt2PngViaPdf,"+coreVersion+",directAccessUrl,imageOptions",
|
||||
"directAccessUrl,docOptions,imageOptions", mockMvc, objectMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTransformEndpointUsingTransformEngineWithTwoCustomTransformers() throws Exception
|
||||
{
|
||||
mockMvc.perform(
|
||||
MockMvcRequestBuilders.multipart(ENDPOINT_TRANSFORM)
|
||||
.file(new MockMultipartFile("file", null, MIMETYPE_TEXT_PLAIN,
|
||||
"Start".getBytes(StandardCharsets.UTF_8)))
|
||||
.param(SOURCE_MIMETYPE, MIMETYPE_TEXT_PLAIN)
|
||||
.param(TARGET_MIMETYPE, MIMETYPE_PDF)
|
||||
.param(PAGE_REQUEST_PARAM, "1"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string("Content-Disposition",
|
||||
"attachment; filename*=UTF-8''transform.pdf"))
|
||||
.andExpect(content().string("Start -> TxT2Pdf(page=1)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTransformEndpointUsingTransformEngineWithOneCustomTransformer() throws Exception
|
||||
{
|
||||
mockMvc.perform(
|
||||
MockMvcRequestBuilders.multipart(ENDPOINT_TRANSFORM)
|
||||
.file(new MockMultipartFile("file", null, MIMETYPE_PDF,
|
||||
"Start".getBytes(StandardCharsets.UTF_8)))
|
||||
.param(SOURCE_MIMETYPE, MIMETYPE_PDF)
|
||||
.param(TARGET_MIMETYPE, MIMETYPE_IMAGE_JPEG))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string("Content-Disposition",
|
||||
"attachment; filename*=UTF-8''transform.jpeg"))
|
||||
.andExpect(content().string("Start -> Pdf2Jpg()"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,482 @@
|
||||
/*
|
||||
* #%L
|
||||
* Alfresco Transform Core
|
||||
* %%
|
||||
* Copyright (C) 2022 - 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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base;
|
||||
|
||||
import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.Logger;
|
||||
import ch.qos.logback.classic.LoggerContext;
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import ch.qos.logback.core.AppenderBase;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import org.alfresco.transform.base.fakes.FakeTransformEngineWithTwoCustomTransformers;
|
||||
import org.alfresco.transform.base.fakes.FakeTransformerPdf2Png;
|
||||
import org.alfresco.transform.base.fakes.FakeTransformerTxT2Pdf;
|
||||
import org.alfresco.transform.base.model.FileRefEntity;
|
||||
import org.alfresco.transform.base.model.FileRefResponse;
|
||||
import org.alfresco.transform.base.sfs.SharedFileStoreClient;
|
||||
import org.alfresco.transform.base.transform.TransformHandler;
|
||||
import org.alfresco.transform.client.model.TransformReply;
|
||||
import org.alfresco.transform.client.model.TransformRequest;
|
||||
import org.alfresco.transform.config.TransformConfig;
|
||||
import org.codehaus.plexus.util.FileUtils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.alfresco.transform.base.AbstractBaseTest.getTestFile;
|
||||
import static org.alfresco.transform.common.Mimetype.MIMETYPE_IMAGE_BMP;
|
||||
import static org.alfresco.transform.common.Mimetype.MIMETYPE_PDF;
|
||||
import static org.alfresco.transform.common.Mimetype.MIMETYPE_TEXT_PLAIN;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_ERROR;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_LIVE;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_LOG;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_READY;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_ROOT;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_TEST;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_TRANSFORM;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_TRANSFORM_CONFIG;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_TRANSFORM_CONFIG_LATEST;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_VERSION;
|
||||
import static org.alfresco.transform.common.RequestParamMap.PAGE_REQUEST_PARAM;
|
||||
import static org.alfresco.transform.common.RequestParamMap.SOURCE_ENCODING;
|
||||
import static org.alfresco.transform.common.RequestParamMap.SOURCE_MIMETYPE;
|
||||
import static org.alfresco.transform.common.RequestParamMap.TARGET_MIMETYPE;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.http.HttpHeaders.ACCEPT;
|
||||
import static org.springframework.http.HttpHeaders.CONTENT_DISPOSITION;
|
||||
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
|
||||
import static org.springframework.http.HttpStatus.CREATED;
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Tests the endpoints of the TransformController.
|
||||
*
|
||||
* Also see {@link TransformControllerAllInOneTest}.
|
||||
*/
|
||||
@AutoConfigureMockMvc
|
||||
@SpringBootTest(classes={org.alfresco.transform.base.Application.class})
|
||||
@ContextConfiguration(classes = {
|
||||
FakeTransformEngineWithTwoCustomTransformers.class,
|
||||
FakeTransformerTxT2Pdf.class,
|
||||
FakeTransformerPdf2Png.class})
|
||||
public class TransformControllerTest
|
||||
{
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
@Autowired
|
||||
private TransformController transformController;
|
||||
@Autowired
|
||||
protected ObjectMapper objectMapper;
|
||||
@Autowired
|
||||
private String coreVersion;
|
||||
@TempDir
|
||||
public File tempDir;
|
||||
@MockBean
|
||||
protected SharedFileStoreClient fakeSfsClient;
|
||||
|
||||
static void resetProbeForTesting(TransformController transformController)
|
||||
{
|
||||
AbstractBaseTest.resetProbeForTesting(transformController.getProbeTransform());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitEngine() throws Exception
|
||||
{
|
||||
assertEquals(FakeTransformEngineWithTwoCustomTransformers.class.getSimpleName(),
|
||||
transformController.transformEngine.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStartupLogsIncludeEngineMessages()
|
||||
{
|
||||
StringJoiner controllerLogMessages = getLogMessagesFor(TransformController.class);
|
||||
|
||||
transformController.startup();
|
||||
|
||||
assertEquals(
|
||||
"--------------------------------------------------------------------------------------------------------------------------------------------------------------\n"
|
||||
+ "Startup 0000 TwoCustomTransformers\n"
|
||||
+ "Line 2 0000 TwoCustomTransformers\n"
|
||||
+ "Line 3\n"
|
||||
+ "--------------------------------------------------------------------------------------------------------------------------------------------------------------\n"
|
||||
+ "Starting application components... Done",
|
||||
controllerLogMessages.toString());
|
||||
}
|
||||
|
||||
public static StringJoiner getLogMessagesFor(Class classBeingLogged)
|
||||
{
|
||||
StringJoiner logMessages = new StringJoiner("\n");
|
||||
Logger logger = (Logger) LoggerFactory.getLogger(classBeingLogged);
|
||||
AppenderBase<ILoggingEvent> logAppender = new AppenderBase<>()
|
||||
{
|
||||
@Override
|
||||
protected void append(ILoggingEvent iLoggingEvent)
|
||||
{
|
||||
logMessages.add(iLoggingEvent.getMessage());
|
||||
}
|
||||
};
|
||||
logAppender.setContext((LoggerContext)LoggerFactory.getILoggerFactory());
|
||||
logger.setLevel(Level.DEBUG);
|
||||
logger.addAppender(logAppender);
|
||||
logAppender.start();
|
||||
|
||||
return logMessages;
|
||||
}
|
||||
|
||||
|
||||
private void testPageWithOrWithoutIngresPrefix(String url, boolean behindIngres, String... expected) throws Exception
|
||||
{
|
||||
boolean origBehindIngres = (boolean) ReflectionTestUtils.getField(transformController, "behindIngres");
|
||||
try
|
||||
{
|
||||
ReflectionTestUtils.setField(transformController, "behindIngres", behindIngres);
|
||||
|
||||
mockMvc.perform(MockMvcRequestBuilders.get(url))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(containsString(expected[0])))
|
||||
.andExpect(content().string(containsString(expected[1])))
|
||||
.andExpect(content().string(containsString(expected[2])))
|
||||
.andExpect(content().string(containsString(expected[3])))
|
||||
.andExpect(content().string(containsString(expected[4])))
|
||||
.andExpect(content().string(containsString(expected[5])));
|
||||
}
|
||||
finally
|
||||
{
|
||||
ReflectionTestUtils.setField(transformController, "behindIngres", origBehindIngres);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVersionEndpointIncludesAvailable() throws Exception
|
||||
{
|
||||
mockMvc.perform(MockMvcRequestBuilders.get(ENDPOINT_VERSION))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string("TwoCustomTransformers "+coreVersion));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRootEndpointReturnsTestPage() throws Exception
|
||||
{
|
||||
testPageWithOrWithoutIngresPrefix(ENDPOINT_ROOT, false,
|
||||
"TwoCustomTransformers Test Page",
|
||||
"action=\"/test\"",
|
||||
"<a href=\"/log\">Log</a>",
|
||||
"<a href=\"/ready\">Ready</a>",
|
||||
"<a href=\"/live\">Live</a>",
|
||||
"<a href=\"/transform/config?configVersion=9999\">Config</a>");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRootEndpointReturnsTestPageWithIngres() throws Exception
|
||||
{
|
||||
testPageWithOrWithoutIngresPrefix(ENDPOINT_ROOT, true,
|
||||
"TwoCustomTransformers Test Page",
|
||||
"action=\"/twocustomtransformers/test\"",
|
||||
"href=\"/twocustomtransformers/log\"",
|
||||
"<a href=\"/twocustomtransformers/ready\">Ready</a>",
|
||||
"<a href=\"/twocustomtransformers/live\">Live</a>",
|
||||
"<a href=\"/twocustomtransformers/transform/config?configVersion=9999\">Config</a>");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testErrorEndpointReturnsErrorPage() throws Exception
|
||||
{
|
||||
testPageWithOrWithoutIngresPrefix(ENDPOINT_ERROR, false,
|
||||
"TwoCustomTransformers Error Page",
|
||||
"<a href=\"/\">Test</a>",
|
||||
"<a href=\"/log\">Log</a>",
|
||||
"<a href=\"/ready\">Ready</a>",
|
||||
"<a href=\"/live\">Live</a>",
|
||||
"<a href=\"/transform/config?configVersion=9999\">Config</a>"); }
|
||||
|
||||
@Test
|
||||
public void testErrorEndpointReturnsErrorPageWithIngres() throws Exception
|
||||
{
|
||||
testPageWithOrWithoutIngresPrefix(ENDPOINT_ERROR, true,
|
||||
"TwoCustomTransformers Error Page",
|
||||
"href=\"/twocustomtransformers/\"",
|
||||
"href=\"/twocustomtransformers/log\"",
|
||||
"<a href=\"/twocustomtransformers/ready\">Ready</a>",
|
||||
"<a href=\"/twocustomtransformers/live\">Live</a>",
|
||||
"<a href=\"/twocustomtransformers/transform/config?configVersion=9999\">Config</a>");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLogEndpointReturnsLogPage() throws Exception
|
||||
{
|
||||
testPageWithOrWithoutIngresPrefix(ENDPOINT_LOG, false,
|
||||
"TwoCustomTransformers Log Entries",
|
||||
"<a href=\"/\">Test</a>",
|
||||
"Log",
|
||||
"<a href=\"/ready\">Ready</a>",
|
||||
"<a href=\"/live\">Live</a>",
|
||||
"<a href=\"/transform/config?configVersion=9999\">Config</a>");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLogEndpointReturnsLogPageWithIngres() throws Exception
|
||||
{
|
||||
testPageWithOrWithoutIngresPrefix(ENDPOINT_LOG, true,
|
||||
"TwoCustomTransformers Log Entries",
|
||||
"href=\"/twocustomtransformers/\"",
|
||||
"Log",
|
||||
"<a href=\"/twocustomtransformers/ready\">Ready</a>",
|
||||
"<a href=\"/twocustomtransformers/live\">Live</a>",
|
||||
"<a href=\"/twocustomtransformers/transform/config?configVersion=9999\">Config</a>");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadyEndpointReturnsSuccessful() throws Exception
|
||||
{
|
||||
resetProbeForTesting(transformController);
|
||||
mockMvc.perform(MockMvcRequestBuilders.get(ENDPOINT_READY))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(containsString("Success - ")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLiveEndpointReturnsSuccessful() throws Exception
|
||||
{
|
||||
resetProbeForTesting(transformController);
|
||||
mockMvc.perform(MockMvcRequestBuilders.get(ENDPOINT_LIVE))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(containsString("Success - ")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConfigEndpointReturnsOriginalConfigFormat() throws Exception
|
||||
{
|
||||
// Includes Txt2PngViaPdf as Pdf2Jpg might exist in another t-engine
|
||||
// coreValue is not set as this is the default version of config
|
||||
// The transformer's options should not include directAccessUrl as this is the default version of config
|
||||
assertConfig(ENDPOINT_TRANSFORM_CONFIG,
|
||||
"Pdf2Png,null,imageOptions\n"
|
||||
+ "TxT2Pdf,null,docOptions\n"
|
||||
+ "Txt2JpgViaPdf,null,imageOptions\n"
|
||||
+ "Txt2PngViaPdf,null,imageOptions",
|
||||
"docOptions,imageOptions", mockMvc, objectMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConfigLatestEndpointReturnsCoreVersionAndDirectAccessUrlOption() throws Exception
|
||||
{
|
||||
assertConfig(ENDPOINT_TRANSFORM_CONFIG_LATEST,
|
||||
"Pdf2Png,"+coreVersion+",directAccessUrl,imageOptions\n"
|
||||
+ "TxT2Pdf,"+coreVersion+",directAccessUrl,docOptions\n"
|
||||
+ "Txt2JpgViaPdf,null,imageOptions\n"
|
||||
+ "Txt2PngViaPdf,"+coreVersion+",directAccessUrl,imageOptions",
|
||||
"directAccessUrl,docOptions,imageOptions", mockMvc, objectMapper);
|
||||
}
|
||||
|
||||
static void assertConfig(String url, String expectedTransformers, String expectedOptions,
|
||||
MockMvc mockMvc, ObjectMapper objectMapper) throws Exception
|
||||
{
|
||||
TransformConfig config = objectMapper.readValue(
|
||||
mockMvc.perform(MockMvcRequestBuilders.get(url))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string(CONTENT_TYPE, APPLICATION_JSON_VALUE))
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString(), TransformConfig.class);
|
||||
|
||||
// Gets a list of transformerNames,coreVersion,optionNames
|
||||
assertEquals(expectedTransformers,
|
||||
config.getTransformers().stream()
|
||||
.map(t -> t.getTransformerName()+","
|
||||
+t.getCoreVersion()+","
|
||||
+t.getTransformOptions().stream().sorted().collect(Collectors.joining(",")))
|
||||
.sorted()
|
||||
.collect(Collectors.joining("\n")));
|
||||
|
||||
assertEquals(expectedOptions,
|
||||
config.getTransformOptions().keySet().stream()
|
||||
.sorted()
|
||||
.collect(Collectors.joining(",")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTransformEndpointThatUsesTransformRequests() throws Exception
|
||||
{
|
||||
final Map<String,File> sfsRef2File = new HashMap<>();
|
||||
when(fakeSfsClient.saveFile(any())).thenAnswer((Answer) invocation -> {
|
||||
File originalFile = (File) invocation.getArguments()[0];
|
||||
|
||||
// Make a copy as the original might get deleted
|
||||
File fileCopy = new File(tempDir, originalFile.getName()+"copy");
|
||||
FileUtils.copyFile(originalFile, fileCopy);
|
||||
|
||||
String fileRef = UUID.randomUUID().toString();
|
||||
sfsRef2File.put(fileRef, fileCopy);
|
||||
return new FileRefResponse(new FileRefEntity(fileRef));
|
||||
});
|
||||
|
||||
when(fakeSfsClient.retrieveFile(any())).thenAnswer((Answer) invocation ->
|
||||
ResponseEntity.ok().header(CONTENT_DISPOSITION,"attachment; filename*=UTF-8''transform.tmp")
|
||||
.body((Resource) new UrlResource(sfsRef2File.get(invocation.getArguments()[0]).toURI())));
|
||||
|
||||
File sourceFile = getTestFile("original.txt", true, tempDir);
|
||||
String sourceFileRef = fakeSfsClient.saveFile(sourceFile).getEntry().getFileRef();
|
||||
|
||||
TransformRequest transformRequest = TransformRequest.builder()
|
||||
.withRequestId("1")
|
||||
.withSchema(1)
|
||||
.withClientData("Alfresco Digital Business Platform")
|
||||
// .withTransformRequestOptions(ImmutableMap.of(DIRECT_ACCESS_URL, "file://"+sourceFile.toPath()))
|
||||
.withSourceReference(sourceFileRef)
|
||||
.withSourceMediaType(MIMETYPE_TEXT_PLAIN)
|
||||
.withSourceSize(sourceFile.length())
|
||||
.withTargetMediaType(MIMETYPE_PDF)
|
||||
.withInternalContextForTransformEngineTests()
|
||||
.build();
|
||||
|
||||
String transformRequestJson = objectMapper.writeValueAsString(transformRequest);
|
||||
String transformReplyJson = mockMvc
|
||||
.perform(MockMvcRequestBuilders
|
||||
.post(ENDPOINT_TRANSFORM)
|
||||
.header(ACCEPT, APPLICATION_JSON_VALUE)
|
||||
.header(CONTENT_TYPE, APPLICATION_JSON_VALUE)
|
||||
.content(transformRequestJson))
|
||||
.andExpect(status().is(CREATED.value()))
|
||||
.andReturn().getResponse().getContentAsString();
|
||||
TransformReply transformReply = objectMapper.readValue(transformReplyJson, TransformReply.class);
|
||||
String newValue = new String(fakeSfsClient.retrieveFile(transformReply.getTargetReference()).getBody()
|
||||
.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||
|
||||
assertEquals(transformRequest.getRequestId(), transformReply.getRequestId());
|
||||
assertEquals(transformRequest.getClientData(), transformReply.getClientData());
|
||||
assertEquals(transformRequest.getSchema(), transformReply.getSchema());
|
||||
assertEquals("Original Text -> TxT2Pdf()", newValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTransformEndpointThatUploadsAndDownloadsContent() throws Exception
|
||||
{
|
||||
mockMvc.perform(
|
||||
MockMvcRequestBuilders.multipart(ENDPOINT_TRANSFORM)
|
||||
.file(new MockMultipartFile("file", null, MIMETYPE_TEXT_PLAIN,
|
||||
"Start".getBytes(StandardCharsets.UTF_8)))
|
||||
.param(SOURCE_MIMETYPE, MIMETYPE_TEXT_PLAIN)
|
||||
.param(TARGET_MIMETYPE, MIMETYPE_PDF)
|
||||
.param(PAGE_REQUEST_PARAM, "1"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string("Content-Disposition",
|
||||
"attachment; filename*=UTF-8''transform.pdf"))
|
||||
.andExpect(content().string("Start -> TxT2Pdf(page=1)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTestTransformEndpointWhichConvertsRequestParameters() throws Exception
|
||||
{
|
||||
TransformHandler transformHandlerOrig = transformController.transformHandler;
|
||||
try
|
||||
{
|
||||
TransformHandler transformHandlerSpy = spy(transformHandlerOrig);
|
||||
transformController.transformHandler = transformHandlerSpy;
|
||||
|
||||
mockMvc.perform(
|
||||
MockMvcRequestBuilders.multipart(ENDPOINT_TEST)
|
||||
.file(new MockMultipartFile("file", null, MIMETYPE_TEXT_PLAIN,
|
||||
"Start".getBytes(StandardCharsets.UTF_8)))
|
||||
.param(SOURCE_MIMETYPE, MIMETYPE_IMAGE_BMP)
|
||||
.param("_"+SOURCE_MIMETYPE, MIMETYPE_TEXT_PLAIN)
|
||||
.param(TARGET_MIMETYPE, MIMETYPE_PDF)
|
||||
.param("_"+TARGET_MIMETYPE, "")
|
||||
.param(PAGE_REQUEST_PARAM, "replaced")
|
||||
.param("name1", "hasNoValueSoRemoved").param("value1", "")
|
||||
.param("name2", PAGE_REQUEST_PARAM).param("value2", "1")
|
||||
.param("name3", SOURCE_ENCODING).param("value3", "UTF-8"));
|
||||
|
||||
verify(transformHandlerSpy).handleHttpRequest(any(), any(), eq(MIMETYPE_TEXT_PLAIN), eq(MIMETYPE_PDF),
|
||||
eq(ImmutableMap.of(
|
||||
SOURCE_MIMETYPE, MIMETYPE_TEXT_PLAIN,
|
||||
TARGET_MIMETYPE, MIMETYPE_PDF,
|
||||
PAGE_REQUEST_PARAM, "1",
|
||||
SOURCE_ENCODING, "UTF-8")), any());
|
||||
}
|
||||
finally
|
||||
{
|
||||
transformController.transformHandler = transformHandlerOrig;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInterceptOfMissingServletRequestParameterException() throws Exception
|
||||
{
|
||||
mockMvc.perform(
|
||||
MockMvcRequestBuilders.multipart(ENDPOINT_TRANSFORM)
|
||||
.file(new MockMultipartFile("file", null, MIMETYPE_TEXT_PLAIN,
|
||||
"Start".getBytes(StandardCharsets.UTF_8))))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(status().reason(containsString("Request parameter '"+SOURCE_MIMETYPE+"' is missing")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInterceptOfTransformException_noTransformers() throws Exception
|
||||
{
|
||||
mockMvc.perform(
|
||||
MockMvcRequestBuilders.multipart(ENDPOINT_TRANSFORM)
|
||||
.file(new MockMultipartFile("file", null, MIMETYPE_TEXT_PLAIN,
|
||||
"Start".getBytes(StandardCharsets.UTF_8)))
|
||||
.param(SOURCE_MIMETYPE, MIMETYPE_TEXT_PLAIN)
|
||||
.param(TARGET_MIMETYPE, MIMETYPE_PDF)
|
||||
.param("unknown", "1"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(content().string(containsString("TwoCustomTransformers Error Page")))
|
||||
.andExpect(content().string(containsString("No transforms for:")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* #%L
|
||||
* Alfresco Transform Core
|
||||
* %%
|
||||
* Copyright (C) 2005 - 2019 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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.clients;
|
||||
|
||||
/**
|
||||
* @author Cezar Leahu
|
||||
*/
|
||||
public class FileInfo
|
||||
{
|
||||
private final String mimeType;
|
||||
private final String extension;
|
||||
private final String path;
|
||||
private final boolean exactMimeType;
|
||||
|
||||
public FileInfo(final String mimeType, final String extension, final String path, final boolean exactMimeType)
|
||||
{
|
||||
this.mimeType = mimeType;
|
||||
this.extension = extension;
|
||||
this.path = path;
|
||||
this.exactMimeType = exactMimeType;
|
||||
}
|
||||
|
||||
public String getMimeType()
|
||||
{
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
public String getExtension()
|
||||
{
|
||||
return extension;
|
||||
}
|
||||
|
||||
public String getPath()
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
public boolean isExactMimeType()
|
||||
{
|
||||
return exactMimeType;
|
||||
}
|
||||
|
||||
public static FileInfo testFile(final String mimeType, final String extension, final String path, final boolean exactMimeType)
|
||||
{
|
||||
return new FileInfo(mimeType, extension, path, exactMimeType);
|
||||
}
|
||||
|
||||
public static FileInfo testFile(final String mimeType, final String extension, final String path)
|
||||
{
|
||||
return new FileInfo(mimeType, extension, path, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2015-2022 Alfresco Software, Ltd. All rights reserved.
|
||||
*
|
||||
* License rights for this program may be obtained from Alfresco Software, Ltd.
|
||||
* pursuant to a written agreement and any use of this program without such an
|
||||
* agreement is prohibited.
|
||||
*/
|
||||
package org.alfresco.transform.base.clients;
|
||||
|
||||
import static java.util.Collections.emptyMap;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_TRANSFORM;
|
||||
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
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.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* @author Cezar Leahu
|
||||
*/
|
||||
public class HttpClient
|
||||
{
|
||||
private static final RestTemplate REST_TEMPLATE = new RestTemplate();
|
||||
|
||||
public static ResponseEntity<Resource> sendTRequest(
|
||||
final String engineUrl, final String sourceFile,
|
||||
final String sourceMimetype, final String targetMimetype, final String targetExtension)
|
||||
{
|
||||
return sendTRequest(engineUrl, sourceFile, sourceMimetype, targetMimetype, targetExtension, emptyMap());
|
||||
}
|
||||
|
||||
public static ResponseEntity<Resource> sendTRequest(
|
||||
final String engineUrl, final String sourceFile,
|
||||
final String sourceMimetype, final String targetMimetype, final String targetExtension,
|
||||
final Map<String, String> transformOptions)
|
||||
{
|
||||
final HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MULTIPART_FORM_DATA);
|
||||
|
||||
final MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
body.add("file", new ClassPathResource(sourceFile));
|
||||
if (sourceMimetype != null && !sourceMimetype.trim().isEmpty())
|
||||
{
|
||||
body.add("sourceMimetype", sourceMimetype);
|
||||
}
|
||||
if (targetMimetype != null && !targetMimetype.trim().isEmpty())
|
||||
{
|
||||
body.add("targetMimetype", targetMimetype);
|
||||
}
|
||||
if (targetExtension != null && !targetExtension.trim().isEmpty())
|
||||
{
|
||||
body.add("targetExtension", targetExtension);
|
||||
}
|
||||
transformOptions.forEach(body::add);
|
||||
|
||||
final HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
|
||||
|
||||
return REST_TEMPLATE.postForEntity(engineUrl + ENDPOINT_TRANSFORM, entity, Resource.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2015-2022 Alfresco Software, Ltd. All rights reserved.
|
||||
*
|
||||
* License rights for this program may be obtained from Alfresco Software, Ltd.
|
||||
* pursuant to a written agreement and any use of this program without such an
|
||||
* agreement is prohibited.
|
||||
*/
|
||||
|
||||
package org.alfresco.transform.base.clients;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.MapperFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
public class JacksonSerializer
|
||||
{
|
||||
private static final ObjectMapper MAPPER;
|
||||
|
||||
static
|
||||
{
|
||||
MAPPER = new ObjectMapper();
|
||||
MAPPER.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false);
|
||||
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
MAPPER.setSerializationInclusion(Include.NON_NULL);
|
||||
}
|
||||
|
||||
public static <T> byte[] serialize(T value) throws Exception
|
||||
{
|
||||
try (final ByteArrayOutputStream stream = new ByteArrayOutputStream(1024);
|
||||
final OutputStreamWriter writer = new OutputStreamWriter(stream, UTF_8))
|
||||
{
|
||||
MAPPER.writer().writeValue(writer, value);
|
||||
return stream.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> T deserialize(byte[] data, Class<T> cls) throws Exception
|
||||
{
|
||||
return MAPPER.readValue(data, cls);
|
||||
}
|
||||
|
||||
public static <T> T deserialize(byte[] data, int len, Class<T> cls) throws Exception
|
||||
{
|
||||
return MAPPER.readValue(data, 0, len, cls);
|
||||
}
|
||||
|
||||
public static String readStringValue(String json, String key) throws Exception
|
||||
{
|
||||
JsonNode node = MAPPER.readTree(json);
|
||||
for (String k : key.split("\\."))
|
||||
{
|
||||
node = node.get(k);
|
||||
}
|
||||
return node.asText();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* Copyright 2015-2022 Alfresco Software, Ltd. All rights reserved.
|
||||
*
|
||||
* License rights for this program may be obtained from Alfresco Software, Ltd.
|
||||
* pursuant to a written agreement and any use of this program without such an
|
||||
* agreement is prohibited.
|
||||
*/
|
||||
|
||||
package org.alfresco.transform.base.clients;
|
||||
|
||||
import javax.jms.BytesMessage;
|
||||
import javax.jms.Connection;
|
||||
import javax.jms.ConnectionFactory;
|
||||
import javax.jms.DeliveryMode;
|
||||
import javax.jms.Destination;
|
||||
import javax.jms.MessageConsumer;
|
||||
import javax.jms.MessageProducer;
|
||||
import javax.jms.Session;
|
||||
import javax.jms.TextMessage;
|
||||
|
||||
import org.alfresco.transform.client.model.TransformReply;
|
||||
import org.alfresco.transform.client.model.TransformRequest;
|
||||
import org.apache.activemq.ActiveMQConnection;
|
||||
import org.apache.activemq.ActiveMQConnectionFactory;
|
||||
import org.apache.activemq.command.ActiveMQQueue;
|
||||
|
||||
/**
|
||||
* JMSClient
|
||||
*
|
||||
* Contains the bare minimum logic necessary for sending and receiving T-Request/Reply messages
|
||||
* through the basic vanilla ActiveMQ client.
|
||||
*
|
||||
* Used by Aspose t-engine and t-router, but likely to be useful in other t-engines.
|
||||
*
|
||||
* @author Cezar Leahu
|
||||
*/
|
||||
public class JmsClient
|
||||
{
|
||||
private final ConnectionFactory factory;
|
||||
private final ActiveMQQueue queue;
|
||||
|
||||
public JmsClient(final String server, final String queueName)
|
||||
{
|
||||
factory = new ActiveMQConnectionFactory(server);
|
||||
queue = new ActiveMQQueue(queueName);
|
||||
}
|
||||
|
||||
public ActiveMQQueue getDestination()
|
||||
{
|
||||
return queue;
|
||||
}
|
||||
|
||||
public void sendBytesMessage(final TransformRequest request)
|
||||
throws Exception
|
||||
{
|
||||
sendBytesMessage(request, request.getRequestId());
|
||||
}
|
||||
|
||||
public void sendBytesMessage(final TransformRequest request, final String correlationID)
|
||||
throws Exception
|
||||
{
|
||||
sendBytesMessage(JacksonSerializer.serialize(request), correlationID);
|
||||
}
|
||||
|
||||
public void sendBytesMessage(final TransformRequest request, final String correlationID,
|
||||
final Destination replyTo) throws Exception
|
||||
{
|
||||
sendBytesMessage(JacksonSerializer.serialize(request), correlationID, replyTo);
|
||||
}
|
||||
|
||||
public void sendBytesMessage(final byte[] data, final String correlationID) throws
|
||||
Exception
|
||||
{
|
||||
try (final Connection connection = factory.createConnection();
|
||||
final Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
|
||||
final MessageProducer producer = session.createProducer(queue))
|
||||
{
|
||||
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
|
||||
final BytesMessage message = session.createBytesMessage();
|
||||
message.writeBytes(data);
|
||||
if (correlationID != null)
|
||||
{
|
||||
message.setJMSCorrelationID(correlationID);
|
||||
}
|
||||
producer.send(message);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendBytesMessage(final byte[] data, final String correlationID,
|
||||
final Destination replyTo) throws Exception
|
||||
{
|
||||
try (final Connection connection = factory.createConnection();
|
||||
final Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
|
||||
final MessageProducer producer = session.createProducer(queue))
|
||||
{
|
||||
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
|
||||
final BytesMessage message = session.createBytesMessage();
|
||||
message.writeBytes(data);
|
||||
if (correlationID != null)
|
||||
{
|
||||
message.setJMSCorrelationID(correlationID);
|
||||
}
|
||||
if (replyTo != null)
|
||||
{
|
||||
message.setJMSReplyTo(replyTo);
|
||||
}
|
||||
producer.send(message);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendTextMessage(final TransformRequest request)
|
||||
throws Exception
|
||||
{
|
||||
sendTextMessage(request, request.getRequestId());
|
||||
}
|
||||
|
||||
public void sendTextMessage(final TransformRequest request, final String correlationID)
|
||||
throws Exception
|
||||
{
|
||||
sendTextMessage(new String(JacksonSerializer.serialize(request)), correlationID);
|
||||
}
|
||||
|
||||
public void sendTextMessage(final String data, final String correlationID) throws
|
||||
Exception
|
||||
{
|
||||
try (final Connection connection = factory.createConnection();
|
||||
final Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
|
||||
final MessageProducer producer = session.createProducer(queue))
|
||||
{
|
||||
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
|
||||
final TextMessage message = session.createTextMessage(data);
|
||||
if (correlationID != null)
|
||||
{
|
||||
message.setJMSCorrelationID(correlationID);
|
||||
}
|
||||
producer.send(message);
|
||||
}
|
||||
}
|
||||
|
||||
public TransformReply receiveMessage() throws Exception
|
||||
{
|
||||
return receiveMessage(2 * 60 * 1000); // 2 m
|
||||
}
|
||||
|
||||
public TransformReply receiveMessage(final long timeout)
|
||||
throws Exception
|
||||
{
|
||||
try (final Connection connection = factory.createConnection();
|
||||
final Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
|
||||
final MessageConsumer consumer = session.createConsumer(queue))
|
||||
{
|
||||
connection.start();
|
||||
|
||||
final BytesMessage message = (BytesMessage) consumer.receive(timeout);
|
||||
if (message == null)
|
||||
{
|
||||
throw new Exception("No reply was received for the multi-step transform request");
|
||||
}
|
||||
final byte[] data = new byte[2048];
|
||||
int len = message.readBytes(data);
|
||||
return JacksonSerializer.deserialize(data, len, TransformReply.class);
|
||||
}
|
||||
}
|
||||
|
||||
public void cleanQueue()
|
||||
{
|
||||
try
|
||||
{
|
||||
while (receiveMessage(2 * 1000) != null)
|
||||
{
|
||||
}
|
||||
}
|
||||
catch (Exception ignore)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright 2015-2022 Alfresco Software, Ltd. All rights reserved.
|
||||
*
|
||||
* License rights for this program may be obtained from Alfresco Software, Ltd.
|
||||
* pursuant to a written agreement and any use of this program without such an
|
||||
* agreement is prohibited.
|
||||
*/
|
||||
|
||||
package org.alfresco.transform.base.clients;
|
||||
|
||||
import static java.text.MessageFormat.format;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpHead;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.ContentType;
|
||||
import org.apache.http.entity.mime.HttpMultipartMode;
|
||||
import org.apache.http.entity.mime.MultipartEntityBuilder;
|
||||
import org.apache.http.entity.mime.content.FileBody;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.Logger;
|
||||
|
||||
/**
|
||||
* Used by Aspose t-engine and t-router, but likely to be useful in other t-engines.
|
||||
*
|
||||
* @author Cezar Leahu
|
||||
*/
|
||||
public class SfsClient
|
||||
{
|
||||
static
|
||||
{
|
||||
((Logger) LoggerFactory.getLogger("org.apache.http.client.protocol")).setLevel(Level.INFO);
|
||||
((Logger) LoggerFactory.getLogger("org.apache.http.impl.conn")).setLevel(Level.INFO);
|
||||
((Logger) LoggerFactory.getLogger("org.apache.http.headers")).setLevel(Level.INFO);
|
||||
((Logger) LoggerFactory.getLogger("org.apache.http.wire")).setLevel(Level.INFO);
|
||||
((Logger) LoggerFactory.getLogger("org.apache.http.wire")).setAdditive(false);
|
||||
}
|
||||
|
||||
private static final String SFS_BASE_URL = "http://localhost:8099";
|
||||
|
||||
public static String uploadFile(final String fileToUploadName) throws Exception
|
||||
{
|
||||
return uploadFile(fileToUploadName, SFS_BASE_URL);
|
||||
}
|
||||
|
||||
public static String uploadFile(final String fileToUploadName, final String sfsBaseUrl) throws Exception
|
||||
{
|
||||
final File file = readFile(fileToUploadName);
|
||||
|
||||
final HttpPost post = new HttpPost(
|
||||
sfsBaseUrl+"/alfresco/api/-default-/private/sfs/versions/1/file");
|
||||
post.setEntity(MultipartEntityBuilder
|
||||
.create()
|
||||
.setMode(HttpMultipartMode.BROWSER_COMPATIBLE)
|
||||
.addPart("file", new FileBody(file, ContentType.DEFAULT_BINARY))
|
||||
.build());
|
||||
|
||||
try (CloseableHttpClient client = HttpClients.createDefault())
|
||||
{
|
||||
final HttpResponse response = client.execute(post);
|
||||
int status = response.getStatusLine().getStatusCode();
|
||||
if (status >= 200 && status < 300)
|
||||
{
|
||||
return JacksonSerializer.readStringValue(EntityUtils.toString(response.getEntity()),
|
||||
"entry.fileRef");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Failed to upload source file to SFS");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static File readFile(final String filename) throws Exception
|
||||
{
|
||||
final URL url = SfsClient.class.getClassLoader().getResource(filename);
|
||||
if (url == null)
|
||||
{
|
||||
throw new Exception("Failed to load resource URL with filename " + filename);
|
||||
}
|
||||
final URI uri = url.toURI();
|
||||
try
|
||||
{
|
||||
return Paths.get(uri).toFile();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return readFileFromJar(uri);
|
||||
}
|
||||
}
|
||||
|
||||
private static File readFileFromJar(final URI uri) throws Exception
|
||||
{
|
||||
final String[] array = uri.toString().split("!");
|
||||
try (final FileSystem fs = FileSystems.newFileSystem(URI.create(array[0]),
|
||||
ImmutableMap.of("create", "true")))
|
||||
{
|
||||
File temp = File.createTempFile("temp-", "", new File(System.getProperty("user.dir")));
|
||||
temp.deleteOnExit();
|
||||
Files.copy(fs.getPath(array[1]), temp.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
temp.deleteOnExit();
|
||||
return temp;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean checkFile(final String uuid) throws Exception
|
||||
{
|
||||
return checkFile(uuid, SFS_BASE_URL);
|
||||
}
|
||||
|
||||
public static boolean checkFile(final String uuid, final String sfsBaseUrl) throws Exception
|
||||
{
|
||||
final HttpHead head = new HttpHead(format(
|
||||
sfsBaseUrl+"/alfresco/api/-default-/private/sfs/versions/1/file/{0}",
|
||||
uuid));
|
||||
|
||||
try (CloseableHttpClient client = HttpClients.createDefault())
|
||||
{
|
||||
final HttpResponse response = client.execute(head);
|
||||
final int status = response.getStatusLine().getStatusCode();
|
||||
return status >= 200 && status < 300;
|
||||
}
|
||||
}
|
||||
|
||||
public static File downloadFile(final String uuid) throws Exception
|
||||
{
|
||||
return downloadFile(uuid, SFS_BASE_URL);
|
||||
}
|
||||
|
||||
public static File downloadFile(final String uuid, final String sfsBaseUrl) throws Exception
|
||||
{
|
||||
final HttpGet get = new HttpGet(format(
|
||||
sfsBaseUrl+"/alfresco/api/-default-/private/sfs/versions/1/file/{0}",
|
||||
uuid));
|
||||
|
||||
try (CloseableHttpClient client = HttpClients.createDefault())
|
||||
{
|
||||
final HttpResponse response = client.execute(get);
|
||||
final int status = response.getStatusLine().getStatusCode();
|
||||
if (status < 200 || status >= 300)
|
||||
{
|
||||
throw new Exception("File with UUID " + uuid + " was not found on SFS");
|
||||
}
|
||||
final HttpEntity entity = response.getEntity();
|
||||
if (entity == null)
|
||||
{
|
||||
throw new Exception("Failed to read HTTP reply entity for file with UUID " + uuid);
|
||||
}
|
||||
|
||||
final File file = File.createTempFile(uuid, "_tmp",
|
||||
new File(System.getProperty("user.dir")));
|
||||
file.deleteOnExit();
|
||||
|
||||
try (OutputStream os = new FileOutputStream(file))
|
||||
{
|
||||
entity.writeTo(os);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.clients;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Source & Target media type pair
|
||||
*
|
||||
* @author Cezar Leahu
|
||||
*/
|
||||
public class SourceTarget
|
||||
{
|
||||
public final String source;
|
||||
public final String target;
|
||||
|
||||
private SourceTarget(final String source, final String target)
|
||||
{
|
||||
this.source = source;
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SourceTarget that = (SourceTarget) o;
|
||||
return Objects.equals(source, that.source) &&
|
||||
Objects.equals(target, that.target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hash(source, target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return source + '|' + target;
|
||||
}
|
||||
|
||||
public static SourceTarget of(final String source, final String target)
|
||||
{
|
||||
return new SourceTarget(source, target);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.fakes;
|
||||
|
||||
import org.alfresco.transform.base.TransformEngine;
|
||||
import org.alfresco.transform.base.probes.ProbeTransform;
|
||||
import org.springframework.boot.test.context.TestComponent;
|
||||
|
||||
/**
|
||||
* Subclass MUST be named FakeTransformEngineWith\<something> otherwise the engine name will be "undefined".
|
||||
*/
|
||||
@TestComponent
|
||||
public abstract class AbstractFakeTransformEngine implements TransformEngine
|
||||
{
|
||||
|
||||
private static final String FAKE_TRANSFORM_ENGINE_WITH = "FakeTransformEngineWith";
|
||||
|
||||
@Override public String getTransformEngineName()
|
||||
{
|
||||
String simpleClassName = getClass().getSimpleName();
|
||||
return simpleClassName.startsWith(FAKE_TRANSFORM_ENGINE_WITH)
|
||||
? "0000 "+simpleClassName.substring(FAKE_TRANSFORM_ENGINE_WITH.length())
|
||||
: "undefined";
|
||||
}
|
||||
|
||||
@Override public String getStartupMessage()
|
||||
{
|
||||
return "Startup "+getTransformEngineName()+
|
||||
"\nLine 2 "+getTransformEngineName()+
|
||||
"\nLine 3";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProbeTransform getProbeTransform()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.fakes;
|
||||
|
||||
import org.alfresco.transform.base.CustomTransformer;
|
||||
import org.alfresco.transform.base.TransformManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.test.context.TestComponent;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Subclass MUST be named FakeTransformer\<something>. Appends the name of the CustomTransformer and any t-options
|
||||
* to the output. The output is always a String regardless of the stated mimetypes.
|
||||
*/
|
||||
@TestComponent
|
||||
public abstract class AbstractFakeTransformer implements CustomTransformer
|
||||
{
|
||||
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
@Override
|
||||
public String getTransformerName()
|
||||
{
|
||||
String simpleClassName = getClass().getSimpleName();
|
||||
return simpleClassName.substring("FakeTransformer".length());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transform(String sourceMimetype, InputStream inputStream, String targetMimetype,
|
||||
OutputStream outputStream, Map<String, String> transformOptions, TransformManager transformManager)
|
||||
throws Exception
|
||||
{
|
||||
String oldValue = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
String newValue = new StringBuilder(oldValue)
|
||||
.append(" -> ")
|
||||
.append(getTransformerName())
|
||||
.append("(")
|
||||
.append(transformOptions.entrySet()
|
||||
.stream()
|
||||
.map(e -> e.getKey() + '=' + e.getValue())
|
||||
.collect(Collectors.joining(", ")))
|
||||
.append(')')
|
||||
.toString();
|
||||
logger.info(newValue);
|
||||
byte[] bytes = newValue.getBytes(StandardCharsets.UTF_8);
|
||||
outputStream.write(bytes, 0, bytes.length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.fakes;
|
||||
|
||||
import org.alfresco.transform.base.probes.ProbeTransform;
|
||||
import org.alfresco.transform.config.TransformConfig;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
public class FakeTransformEngineWithAllInOne extends AbstractFakeTransformEngine
|
||||
{
|
||||
@Autowired
|
||||
private FakeTransformEngineWithTwoCustomTransformers oneOfTheTransformEngines;
|
||||
|
||||
@Override public TransformConfig getTransformConfig()
|
||||
{
|
||||
// Has no config of its own. The combined config of the others is returned from the t-engine.
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override public ProbeTransform getProbeTransform()
|
||||
{
|
||||
return oneOfTheTransformEngines.getProbeTransform();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.fakes;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import org.alfresco.transform.base.probes.ProbeTransform;
|
||||
import org.alfresco.transform.config.SupportedSourceAndTarget;
|
||||
import org.alfresco.transform.config.TransformConfig;
|
||||
import org.alfresco.transform.config.Transformer;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.alfresco.transform.common.Mimetype.MIMETYPE_IMAGE_JPEG;
|
||||
import static org.alfresco.transform.common.Mimetype.MIMETYPE_PDF;
|
||||
|
||||
public class FakeTransformEngineWithFragments extends AbstractFakeTransformEngine
|
||||
{
|
||||
@Override public TransformConfig getTransformConfig()
|
||||
{
|
||||
return TransformConfig.builder()
|
||||
.withTransformers(ImmutableList.of(
|
||||
Transformer.builder()
|
||||
.withTransformerName("Fragments")
|
||||
.withSupportedSourceAndTargetList(ImmutableSet.of(
|
||||
SupportedSourceAndTarget.builder()
|
||||
.withSourceMediaType(MIMETYPE_PDF)
|
||||
.withTargetMediaType(MIMETYPE_IMAGE_JPEG)
|
||||
.build()))
|
||||
.build()))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override public ProbeTransform getProbeTransform()
|
||||
{
|
||||
return new ProbeTransform("probe.pdf", MIMETYPE_PDF, MIMETYPE_IMAGE_JPEG, Collections.emptyMap(),
|
||||
60, 16, 400, 10240, 60 * 30 + 1, 60 * 15 + 20);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.fakes;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import org.alfresco.transform.base.probes.ProbeTransform;
|
||||
import org.alfresco.transform.config.SupportedSourceAndTarget;
|
||||
import org.alfresco.transform.config.TransformConfig;
|
||||
import org.alfresco.transform.config.TransformOptionValue;
|
||||
import org.alfresco.transform.config.Transformer;
|
||||
|
||||
import static org.alfresco.transform.common.Mimetype.MIMETYPE_IMAGE_JPEG;
|
||||
import static org.alfresco.transform.common.Mimetype.MIMETYPE_PDF;
|
||||
|
||||
public class FakeTransformEngineWithOneCustomTransformer extends AbstractFakeTransformEngine
|
||||
{
|
||||
@Override public TransformConfig getTransformConfig()
|
||||
{
|
||||
String imageOptions = "imageOptions";
|
||||
return TransformConfig.builder()
|
||||
.withTransformOptions(ImmutableMap.of(
|
||||
imageOptions, ImmutableSet.of(
|
||||
new TransformOptionValue(false, "width"),
|
||||
new TransformOptionValue(false, "height"))))
|
||||
.withTransformers(ImmutableList.of(
|
||||
Transformer.builder()
|
||||
.withTransformerName("Pdf2Jpg")
|
||||
.withSupportedSourceAndTargetList(ImmutableSet.of(
|
||||
SupportedSourceAndTarget.builder()
|
||||
.withSourceMediaType(MIMETYPE_PDF)
|
||||
.withTargetMediaType(MIMETYPE_IMAGE_JPEG)
|
||||
.build()))
|
||||
.withTransformOptions(ImmutableSet.of(imageOptions))
|
||||
.build()))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override public ProbeTransform getProbeTransform()
|
||||
{
|
||||
return null; // Not used in tests
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.fakes;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import org.alfresco.transform.base.probes.ProbeTransform;
|
||||
import org.alfresco.transform.config.SupportedSourceAndTarget;
|
||||
import org.alfresco.transform.config.TransformConfig;
|
||||
import org.alfresco.transform.config.TransformOptionValue;
|
||||
import org.alfresco.transform.config.TransformStep;
|
||||
import org.alfresco.transform.config.Transformer;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.alfresco.transform.common.Mimetype.MIMETYPE_IMAGE_JPEG;
|
||||
import static org.alfresco.transform.common.Mimetype.MIMETYPE_IMAGE_PNG;
|
||||
import static org.alfresco.transform.common.Mimetype.MIMETYPE_PDF;
|
||||
import static org.alfresco.transform.common.Mimetype.MIMETYPE_TEXT_PLAIN;
|
||||
import static org.alfresco.transform.common.RequestParamMap.SOURCE_ENCODING;
|
||||
|
||||
public class FakeTransformEngineWithTwoCustomTransformers extends AbstractFakeTransformEngine
|
||||
{
|
||||
@Override
|
||||
public TransformConfig getTransformConfig()
|
||||
{
|
||||
String docOptions = "docOptions";
|
||||
String imageOptions = "imageOptions";
|
||||
return TransformConfig.builder()
|
||||
.withTransformOptions(ImmutableMap.of(
|
||||
docOptions, ImmutableSet.of(
|
||||
new TransformOptionValue(false, "page")),
|
||||
imageOptions, ImmutableSet.of(
|
||||
new TransformOptionValue(false, "width"),
|
||||
new TransformOptionValue(false, "height"))))
|
||||
.withTransformers(ImmutableList.of(
|
||||
Transformer.builder()
|
||||
.withTransformerName("TxT2Pdf")
|
||||
.withSupportedSourceAndTargetList(ImmutableSet.of(
|
||||
SupportedSourceAndTarget.builder()
|
||||
.withSourceMediaType(MIMETYPE_TEXT_PLAIN)
|
||||
.withTargetMediaType(MIMETYPE_PDF)
|
||||
.build()))
|
||||
.withTransformOptions(ImmutableSet.of(docOptions))
|
||||
.build(),
|
||||
Transformer.builder()
|
||||
.withTransformerName("Pdf2Png")
|
||||
.withSupportedSourceAndTargetList(ImmutableSet.of(
|
||||
SupportedSourceAndTarget.builder()
|
||||
.withSourceMediaType(MIMETYPE_PDF)
|
||||
.withTargetMediaType(MIMETYPE_IMAGE_PNG)
|
||||
.build()))
|
||||
.withTransformOptions(ImmutableSet.of(imageOptions))
|
||||
.build(),
|
||||
Transformer.builder()
|
||||
.withTransformerName("Txt2PngViaPdf")
|
||||
.withTransformerPipeline(List.of(
|
||||
new TransformStep("TxT2Pdf", MIMETYPE_PDF),
|
||||
new TransformStep("Pdf2Png", null)))
|
||||
.withSupportedSourceAndTargetList(ImmutableSet.of(
|
||||
SupportedSourceAndTarget.builder()
|
||||
.withSourceMediaType(MIMETYPE_TEXT_PLAIN)
|
||||
.withTargetMediaType(MIMETYPE_IMAGE_PNG)
|
||||
.build()))
|
||||
.withTransformOptions(ImmutableSet.of(imageOptions))
|
||||
.build(),
|
||||
Transformer.builder() // Unavailable until Pdf2Jpg is added
|
||||
.withTransformerName("Txt2JpgViaPdf")
|
||||
.withTransformerPipeline(List.of(
|
||||
new TransformStep("TxT2Pdf", MIMETYPE_PDF),
|
||||
new TransformStep("Pdf2Jpg", null)))
|
||||
.withSupportedSourceAndTargetList(ImmutableSet.of(
|
||||
SupportedSourceAndTarget.builder()
|
||||
.withSourceMediaType(MIMETYPE_TEXT_PLAIN)
|
||||
.withTargetMediaType(MIMETYPE_IMAGE_JPEG)
|
||||
.build()))
|
||||
.withTransformOptions(ImmutableSet.of(imageOptions))
|
||||
.build()))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProbeTransform getProbeTransform()
|
||||
{
|
||||
return new ProbeTransform("original.txt", MIMETYPE_TEXT_PLAIN, MIMETYPE_PDF,
|
||||
ImmutableMap.of(SOURCE_ENCODING, "UTF-8"), 46, 0,
|
||||
150, 1024, 1, 60 * 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.fakes;
|
||||
|
||||
import org.alfresco.transform.base.TransformManager;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Returns lines in the supplied input as a sequence of transform result fragments.
|
||||
* - If the current line is {@code "Null"} no output is made and {@code null} is passed as the {@code index} to
|
||||
* {@link TransformManager#respondWithFragment(Integer, boolean)}. The {code finished} parameter is unset.
|
||||
* - If {@code "Finished"}, the text is written and the {code finished} parameter is set.
|
||||
* - If the current line is {@code "NullFinished"} no output is made and {@code null} is passed as the {@code index} to
|
||||
* {@code respondWithFragment}. The {code finished} parameter is set.
|
||||
* - If {@code "Ignored"} it will be written to the output, but the {@code respondWithFragment} method will not be
|
||||
* called, so should be ignored if the final line.
|
||||
* If the input is "WithoutFragments", {@code respondWithFragment} is not called.
|
||||
*/
|
||||
public class FakeTransformerFragments extends AbstractFakeTransformer
|
||||
{
|
||||
@Override
|
||||
public void transform(String sourceMimetype, InputStream inputStream, String targetMimetype,
|
||||
OutputStream outputStream, Map<String, String> transformOptions, TransformManager transformManager)
|
||||
throws Exception
|
||||
{
|
||||
String input = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
String[] lines = input.split("\n");
|
||||
if ("WithoutFragments".equals(input))
|
||||
{
|
||||
write(outputStream, input);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < lines.length; i++)
|
||||
{
|
||||
String line = lines[i];
|
||||
Integer index = "Null".equals(line) || "NullFinished".equals(line) ? null : i;
|
||||
boolean finished = "Finished".equals(line) || "NullFinished".equals(line);
|
||||
if (index != null)
|
||||
{
|
||||
write(outputStream, line);
|
||||
}
|
||||
if (!"Ignored".equals(line)) {
|
||||
outputStream = transformManager.respondWithFragment(index, finished);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void write(OutputStream outputStream, String text) throws IOException
|
||||
{
|
||||
if (outputStream != null)
|
||||
{
|
||||
byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
|
||||
outputStream.write(bytes, 0, bytes.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.fakes;
|
||||
|
||||
public class FakeTransformerPdf2Jpg extends AbstractFakeTransformer
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.fakes;
|
||||
|
||||
public class FakeTransformerPdf2Png extends AbstractFakeTransformer
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.fakes;
|
||||
|
||||
public class FakeTransformerTxT2Pdf extends AbstractFakeTransformer
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* #%L
|
||||
* Alfresco Transform Core
|
||||
* %%
|
||||
* Copyright (C) 2022 - 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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.http;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import org.alfresco.transform.config.TransformOption;
|
||||
import org.alfresco.transform.config.TransformOptionGroup;
|
||||
import org.alfresco.transform.config.TransformOptionValue;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.alfresco.transform.base.html.OptionsHelper.getOptionNames;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class OptionsHelperTest
|
||||
{
|
||||
@Test
|
||||
public void emptyTest()
|
||||
{
|
||||
Map<String, Set<TransformOption>> transformOptionsByName = Collections.emptyMap();
|
||||
|
||||
assertEquals(Collections.emptySet(), getOptionNames(transformOptionsByName));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleOptionNameWithSingleValue()
|
||||
{
|
||||
Map<String, Set<TransformOption>> transformOptionsByName = ImmutableMap.of("Dummy", ImmutableSet.of(
|
||||
new TransformOptionValue(true, "startPage")));
|
||||
|
||||
assertEquals(ImmutableSet.of("startPage"), getOptionNames(transformOptionsByName));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenOptionNameEndsInOptions_stripIt()
|
||||
{
|
||||
Map<String, Set<TransformOption>> transformOptionsByName = ImmutableMap.of("DummyOptions", ImmutableSet.of(
|
||||
new TransformOptionValue(true, "startPage")));
|
||||
|
||||
assertEquals(ImmutableSet.of("startPage"), getOptionNames(transformOptionsByName));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleOptionNameWithASingleRequiredValue()
|
||||
{
|
||||
Map<String, Set<TransformOption>> transformOptionsByName = ImmutableMap.of("DummyOptions", ImmutableSet.of(
|
||||
new TransformOptionValue(true, "startPage")));
|
||||
|
||||
assertEquals(ImmutableSet.of("startPage"), getOptionNames(transformOptionsByName));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleOptionNameWithACoupleOfValues()
|
||||
{
|
||||
Map<String, Set<TransformOption>> transformOptionsByName = ImmutableMap.of("DummyOptions", ImmutableSet.of(
|
||||
new TransformOptionValue(false, "startPage"),
|
||||
new TransformOptionValue(true, "endPage")));
|
||||
|
||||
assertEquals(ImmutableSet.of("startPage", "endPage"), getOptionNames(transformOptionsByName));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sortedValues()
|
||||
{
|
||||
Map<String, Set<TransformOption>> transformOptionsByName = ImmutableMap.of("DummyOptions", ImmutableSet.of(
|
||||
new TransformOptionValue(false, "a"),
|
||||
new TransformOptionValue(false, "n"),
|
||||
new TransformOptionValue(false, "k"),
|
||||
new TransformOptionValue(false, "f"),
|
||||
new TransformOptionValue(true, "z")));
|
||||
|
||||
assertEquals(ImmutableList.of("a", "f", "k", "n", "z"), new ArrayList<>(getOptionNames(transformOptionsByName)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleOptionNames()
|
||||
{
|
||||
Map<String, Set<TransformOption>> transformOptionsByName = ImmutableMap.of("DummyOptions", ImmutableSet.of(
|
||||
new TransformOptionValue(false, "startPage"),
|
||||
new TransformOptionValue(true, "endPage")),
|
||||
"Another", ImmutableSet.of(
|
||||
new TransformOptionValue(false, "scale")),
|
||||
"YetAnother", ImmutableSet.of(
|
||||
new TransformOptionValue(false, "x"),
|
||||
new TransformOptionValue(false, "y"),
|
||||
new TransformOptionValue(true, "ratio"))
|
||||
);
|
||||
|
||||
assertEquals(ImmutableSet.of(
|
||||
"startPage",
|
||||
"endPage",
|
||||
"scale",
|
||||
"x",
|
||||
"y",
|
||||
"ratio"),
|
||||
getOptionNames(transformOptionsByName));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleOptionNamesWithDuplicates()
|
||||
{
|
||||
Map<String, Set<TransformOption>> transformOptionsByName = ImmutableMap.of("DummyOptions", ImmutableSet.of(
|
||||
new TransformOptionValue(false, "startPage"),
|
||||
new TransformOptionValue(true, "endPage")),
|
||||
"Another", ImmutableSet.of(
|
||||
new TransformOptionValue(false, "scale")),
|
||||
"YetAnother", ImmutableSet.of(
|
||||
new TransformOptionValue(false, "x"),
|
||||
new TransformOptionValue(false, "y"),
|
||||
new TransformOptionValue(true, "scale"))
|
||||
);
|
||||
|
||||
assertEquals(ImmutableSet.of(
|
||||
"startPage",
|
||||
"endPage",
|
||||
"scale",
|
||||
"x",
|
||||
"y"),
|
||||
getOptionNames(transformOptionsByName));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nestedGroups()
|
||||
{
|
||||
Map<String, Set<TransformOption>> transformOptionsByName = ImmutableMap.of("DummyOptions", ImmutableSet.of(
|
||||
new TransformOptionValue(false, "1"),
|
||||
new TransformOptionValue(true, "2"),
|
||||
new TransformOptionGroup(false, ImmutableSet.of(
|
||||
new TransformOptionValue(false, "3.1"),
|
||||
new TransformOptionValue(true, "3.2"),
|
||||
new TransformOptionValue(false, "3.3"))),
|
||||
new TransformOptionGroup(true, ImmutableSet.of(
|
||||
new TransformOptionValue(false, "4.1"),
|
||||
new TransformOptionGroup(false, ImmutableSet.of(
|
||||
new TransformOptionValue(false, "4.2.1"),
|
||||
new TransformOptionGroup(true, ImmutableSet.of(
|
||||
new TransformOptionValue(false, "4.2.2.1"))),
|
||||
new TransformOptionValue(true, "4.2.3"))),
|
||||
new TransformOptionValue(false, "4.3")))));
|
||||
|
||||
assertEquals(ImmutableSet.of(
|
||||
"1",
|
||||
"2",
|
||||
"3.1",
|
||||
"3.2",
|
||||
"3.3",
|
||||
"4.1",
|
||||
"4.2.1",
|
||||
"4.2.2.1",
|
||||
"4.2.3",
|
||||
"4.3"),
|
||||
getOptionNames(transformOptionsByName));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.http;
|
||||
|
||||
import org.alfresco.transform.base.fakes.FakeTransformEngineWithTwoCustomTransformers;
|
||||
import org.alfresco.transform.base.fakes.FakeTransformerPdf2Png;
|
||||
import org.alfresco.transform.base.fakes.FakeTransformerTxT2Pdf;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
|
||||
import static org.alfresco.transform.common.Mimetype.MIMETYPE_PDF;
|
||||
import static org.alfresco.transform.common.Mimetype.MIMETYPE_TEXT_PLAIN;
|
||||
import static org.alfresco.transform.common.RequestParamMap.DIRECT_ACCESS_URL;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_TRANSFORM;
|
||||
import static org.alfresco.transform.common.RequestParamMap.SOURCE_MIMETYPE;
|
||||
import static org.alfresco.transform.common.RequestParamMap.TARGET_MIMETYPE;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.springframework.http.HttpMethod.POST;
|
||||
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA;
|
||||
|
||||
/**
|
||||
* Very basic requests to the TransformController using http.
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||
classes={org.alfresco.transform.base.Application.class})
|
||||
@ContextConfiguration(classes = {
|
||||
FakeTransformEngineWithTwoCustomTransformers.class,
|
||||
FakeTransformerTxT2Pdf.class,
|
||||
FakeTransformerPdf2Png.class})
|
||||
public class RestTest
|
||||
{
|
||||
@Autowired
|
||||
private TestRestTemplate restTemplate;
|
||||
|
||||
private static final HttpHeaders HEADERS = new HttpHeaders();
|
||||
static {
|
||||
HEADERS.setContentType(MULTIPART_FORM_DATA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noFileError()
|
||||
{
|
||||
LinkedMultiValueMap<String, Object> parameters = new LinkedMultiValueMap<>();
|
||||
parameters.add(SOURCE_MIMETYPE, MIMETYPE_TEXT_PLAIN);
|
||||
parameters.add(TARGET_MIMETYPE, MIMETYPE_PDF);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(ENDPOINT_TRANSFORM, POST,
|
||||
new HttpEntity<>(parameters, HEADERS), String.class, "");
|
||||
|
||||
assertTrue(response.getBody().contains("Required request part 'file' is not present"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void httpTransformRequestDirectAccessUrlNotFoundTest()
|
||||
{
|
||||
LinkedMultiValueMap<String, Object> parameters = new LinkedMultiValueMap<>();
|
||||
parameters.add(DIRECT_ACCESS_URL, "https://expired/direct/access/url");
|
||||
parameters.add(SOURCE_MIMETYPE, MIMETYPE_TEXT_PLAIN);
|
||||
parameters.add(TARGET_MIMETYPE, MIMETYPE_PDF);
|
||||
parameters.add("file", new org.springframework.core.io.ClassPathResource("original.txt"));
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(ENDPOINT_TRANSFORM, POST,
|
||||
new HttpEntity<>(parameters, HEADERS), String.class, "");
|
||||
|
||||
assertTrue(response.getBody().contains("Direct Access Url not found."));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transform()
|
||||
{
|
||||
LinkedMultiValueMap<String, Object> parameters = new LinkedMultiValueMap<>();
|
||||
parameters.add(SOURCE_MIMETYPE, MIMETYPE_TEXT_PLAIN);
|
||||
parameters.add(TARGET_MIMETYPE, MIMETYPE_PDF);
|
||||
parameters.add("file", new org.springframework.core.io.ClassPathResource("original.txt"));
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(ENDPOINT_TRANSFORM, POST,
|
||||
new HttpEntity<>(parameters, HEADERS), String.class, "");
|
||||
|
||||
assertEquals("Original Text -> TxT2Pdf()", response.getBody());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* #%L
|
||||
* Alfresco Transform Core
|
||||
* %%
|
||||
* Copyright (C) 2005 - 2021 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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.messaging;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import javax.jms.Queue;
|
||||
|
||||
import org.alfresco.transform.client.model.TransformReply;
|
||||
import org.alfresco.transform.client.model.TransformRequest;
|
||||
import org.apache.activemq.command.ActiveMQQueue;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.jms.core.JmsTemplate;
|
||||
|
||||
/**
|
||||
* Checks that a t-engine can respond to its message queue. This is really just checking that
|
||||
* ${queue.engineRequestQueue} has been configured. The transform request can (and does fail
|
||||
* because the shared file store does not exist).
|
||||
*
|
||||
* @author Lucian Tuca
|
||||
* created on 15/01/2019
|
||||
*/
|
||||
@SpringBootTest(classes={org.alfresco.transform.base.Application.class},
|
||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||
properties = {"activemq.url=nio://localhost:61616"})
|
||||
public abstract class AbstractQueueIT
|
||||
{
|
||||
@Autowired
|
||||
private Queue engineRequestQueue;
|
||||
@Autowired
|
||||
private JmsTemplate jmsTemplate;
|
||||
private final ActiveMQQueue testingQueue = new ActiveMQQueue("org.alfresco.transform.engine.IT");
|
||||
|
||||
@Test
|
||||
public void queueTransformServiceIT()
|
||||
{
|
||||
TransformRequest request = buildRequest();
|
||||
|
||||
jmsTemplate.convertAndSend(engineRequestQueue, request, m -> {
|
||||
m.setJMSCorrelationID(request.getRequestId());
|
||||
m.setJMSReplyTo(testingQueue);
|
||||
return m;
|
||||
});
|
||||
|
||||
this.jmsTemplate.setReceiveTimeout(1_000);
|
||||
TransformReply reply = (TransformReply) this.jmsTemplate.receiveAndConvert(testingQueue);
|
||||
|
||||
// The transform may fail (for example the SFS is unavailable), but we check we get a response with the
|
||||
// correct id, so we know that the message was processed.
|
||||
assertEquals(request.getRequestId(), reply.getRequestId());
|
||||
}
|
||||
|
||||
protected abstract TransformRequest buildRequest();
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
package org.alfresco.transform.base.messaging;
|
||||
|
||||
import org.alfresco.transform.base.TransformController;
|
||||
import org.alfresco.transform.client.model.TransformReply;
|
||||
import org.alfresco.transform.client.model.TransformRequest;
|
||||
import org.apache.activemq.command.ActiveMQObjectMessage;
|
||||
import org.apache.activemq.command.ActiveMQQueue;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.jms.support.converter.MessageConversionException;
|
||||
|
||||
import javax.jms.Destination;
|
||||
import javax.jms.JMSException;
|
||||
import javax.jms.Message;
|
||||
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
||||
import static org.springframework.http.HttpStatus.CREATED;
|
||||
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
@SpringBootTest(classes={org.alfresco.transform.base.Application.class})
|
||||
public class QueueTransformServiceTest
|
||||
{
|
||||
@Mock
|
||||
private TransformController transformController;
|
||||
@Mock
|
||||
private TransformMessageConverter transformMessageConverter;
|
||||
@Mock
|
||||
private TransformReplySender transformReplySender;
|
||||
|
||||
@InjectMocks
|
||||
private QueueTransformService queueTransformService;
|
||||
|
||||
@Test
|
||||
public void testWhenReceiveNullMessageThenStopFlow()
|
||||
{
|
||||
queueTransformService.receive(null);
|
||||
|
||||
verifyNoInteractions(transformController);
|
||||
verifyNoInteractions(transformMessageConverter);
|
||||
verifyNoInteractions(transformReplySender);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWhenReceiveMessageWithNoReplyToQueueThenStopFlow()
|
||||
{
|
||||
queueTransformService.receive(new ActiveMQObjectMessage());
|
||||
|
||||
verifyNoInteractions(transformController);
|
||||
verifyNoInteractions(transformMessageConverter);
|
||||
verifyNoInteractions(transformReplySender);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertMessageReturnsNullThenReplyWithInternalServerError() throws JMSException
|
||||
{
|
||||
ActiveMQObjectMessage msg = new ActiveMQObjectMessage();
|
||||
msg.setCorrelationId("1234");
|
||||
ActiveMQQueue destination = new ActiveMQQueue();
|
||||
msg.setJMSReplyTo(destination);
|
||||
|
||||
TransformReply reply = TransformReply
|
||||
.builder()
|
||||
.withStatus(INTERNAL_SERVER_ERROR.value())
|
||||
.withErrorDetails(
|
||||
"JMS exception during T-Request deserialization of message with correlationID "
|
||||
+ msg.getCorrelationId() + ": null")
|
||||
.build();
|
||||
|
||||
doReturn(null).when(transformMessageConverter).fromMessage(msg);
|
||||
|
||||
queueTransformService.receive(msg);
|
||||
|
||||
verify(transformMessageConverter).fromMessage(msg);
|
||||
verify(transformReplySender).send(destination, reply, msg.getCorrelationId());
|
||||
|
||||
verifyNoInteractions(transformController);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertMessageThrowsMessageConversionExceptionThenReplyWithBadRequest()
|
||||
throws JMSException
|
||||
{
|
||||
ActiveMQObjectMessage msg = new ActiveMQObjectMessage();
|
||||
msg.setCorrelationId("1234");
|
||||
ActiveMQQueue destination = new ActiveMQQueue();
|
||||
msg.setJMSReplyTo(destination);
|
||||
|
||||
TransformReply reply = TransformReply
|
||||
.builder()
|
||||
.withStatus(BAD_REQUEST.value())
|
||||
.withErrorDetails(
|
||||
"Message conversion exception during T-Request deserialization of message with correlationID"
|
||||
+ msg.getCorrelationId() + ": null")
|
||||
.build();
|
||||
|
||||
doThrow(MessageConversionException.class).when(transformMessageConverter).fromMessage(msg);
|
||||
|
||||
queueTransformService.receive(msg);
|
||||
|
||||
verify(transformMessageConverter).fromMessage(msg);
|
||||
verify(transformReplySender).send(destination, reply, msg.getCorrelationId());
|
||||
|
||||
verifyNoInteractions(transformController);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertMessageThrowsJMSExceptionThenReplyWithInternalServerError()
|
||||
throws JMSException
|
||||
{
|
||||
ActiveMQObjectMessage msg = new ActiveMQObjectMessage();
|
||||
msg.setCorrelationId("1234");
|
||||
ActiveMQQueue destination = new ActiveMQQueue();
|
||||
msg.setJMSReplyTo(destination);
|
||||
|
||||
TransformReply reply = TransformReply
|
||||
.builder()
|
||||
.withStatus(INTERNAL_SERVER_ERROR.value())
|
||||
.withErrorDetails(
|
||||
"JMSException during T-Request deserialization of message with correlationID " +
|
||||
msg.getCorrelationId() + ": null")
|
||||
.build();
|
||||
|
||||
doThrow(JMSException.class).when(transformMessageConverter).fromMessage(msg);
|
||||
|
||||
queueTransformService.receive(msg);
|
||||
|
||||
verify(transformMessageConverter).fromMessage(msg);
|
||||
verify(transformReplySender).send(destination, reply, msg.getCorrelationId());
|
||||
|
||||
verifyNoInteractions(transformController);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWhenReceiveValidTransformRequestThenReplyWithSuccess() throws JMSException
|
||||
{
|
||||
ActiveMQObjectMessage msg = new ActiveMQObjectMessage();
|
||||
ActiveMQQueue destination = new ActiveMQQueue();
|
||||
msg.setJMSReplyTo(destination);
|
||||
|
||||
TransformRequest request = new TransformRequest();
|
||||
TransformReply reply = TransformReply
|
||||
.builder()
|
||||
.withStatus(CREATED.value())
|
||||
.build();
|
||||
|
||||
doReturn(request).when(transformMessageConverter).fromMessage(msg);
|
||||
doAnswer(invocation -> {transformReplySender.send(destination, reply); return null;})
|
||||
.when(transformController).transform(request, null, destination);
|
||||
|
||||
queueTransformService.receive(msg);
|
||||
|
||||
verify(transformMessageConverter).fromMessage(msg);
|
||||
verify(transformController).transform(request, null, destination);
|
||||
verify(transformReplySender).send(destination, reply);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWhenJMSExceptionOnMessageIsThrownThenStopFlow() throws JMSException
|
||||
{
|
||||
Message msg = mock(Message.class);
|
||||
|
||||
doThrow(JMSException.class).when(msg).getJMSReplyTo();
|
||||
|
||||
queueTransformService.receive(msg);
|
||||
|
||||
verifyNoInteractions(transformController);
|
||||
verifyNoInteractions(transformMessageConverter);
|
||||
verifyNoInteractions(transformReplySender);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWhenExceptionOnCorrelationIdIsThrownThenContinueFlowWithNullCorrelationId()
|
||||
throws JMSException
|
||||
{
|
||||
Message msg = mock(Message.class);
|
||||
Destination destination = mock(Destination.class);
|
||||
|
||||
doThrow(JMSException.class).when(msg).getJMSCorrelationID();
|
||||
doReturn(destination).when(msg).getJMSReplyTo();
|
||||
|
||||
TransformRequest request = new TransformRequest();
|
||||
TransformReply reply = TransformReply
|
||||
.builder()
|
||||
.withStatus(CREATED.value())
|
||||
.build();
|
||||
|
||||
doReturn(request).when(transformMessageConverter).fromMessage(msg);
|
||||
doAnswer(invocation -> {transformReplySender.send(destination, reply); return null;})
|
||||
.when(transformController).transform(request, null, destination);
|
||||
|
||||
queueTransformService.receive(msg);
|
||||
|
||||
verify(transformMessageConverter).fromMessage(msg);
|
||||
verify(transformController).transform(request, null, destination);
|
||||
verify(transformReplySender).send(destination, reply);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* #%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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.metadata;
|
||||
|
||||
import static java.text.MessageFormat.format;
|
||||
import static org.alfresco.transform.base.clients.HttpClient.sendTRequest;
|
||||
import static org.alfresco.transform.common.Mimetype.MIMETYPE_METADATA_EXTRACT;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.springframework.http.HttpStatus.OK;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.alfresco.transform.base.clients.FileInfo;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
/**
|
||||
* Super class of metadata integration tests. Sub classes should provide the following:
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>A method providing a
|
||||
* Stream of test files: {@code public static Stream<FileInfo> engineTransformations()}; </li>
|
||||
* <li> Provide expected json files (<sourceFilename>"_metadata.json") as resources on the classpath.</li>
|
||||
* <li> Override the method {@code testTransformation(FileInfo testFileInfo)} such that it calls
|
||||
* the super method as a {@code @ParameterizedTest} for example:</li> </ul>
|
||||
* <pre>
|
||||
* @ParameterizedTest
|
||||
*
|
||||
* @MethodSource("engineTransformations")
|
||||
*
|
||||
* @Override
|
||||
|
||||
* public void testTransformation(FileInfo testFileInfo)
|
||||
*
|
||||
* {
|
||||
* super.testTransformation(FileInfo testFileInfo)
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @author adavis
|
||||
* @author dedwards
|
||||
*/
|
||||
public abstract class AbstractMetadataExtractsIT
|
||||
{
|
||||
private static final String ENGINE_URL = "http://localhost:8090";
|
||||
// These are normally variable, hence the lowercase.
|
||||
private static final String targetMimetype = MIMETYPE_METADATA_EXTRACT;
|
||||
private static final String targetExtension = "json";
|
||||
|
||||
private final ObjectMapper jsonObjectMapper = new ObjectMapper();
|
||||
|
||||
|
||||
public void testTransformation(FileInfo fileInfo)
|
||||
{
|
||||
final String sourceMimetype = fileInfo.getMimeType();
|
||||
final String sourceFile = fileInfo.getPath();
|
||||
|
||||
final String descriptor = format("Transform ({0}, {1} -> {2}, {3})",
|
||||
sourceFile, sourceMimetype, targetMimetype, targetExtension);
|
||||
|
||||
try
|
||||
{
|
||||
final ResponseEntity<Resource> response = sendTRequest(ENGINE_URL, sourceFile,
|
||||
sourceMimetype, targetMimetype, targetExtension);
|
||||
assertEquals(OK, response.getStatusCode(), descriptor);
|
||||
|
||||
String metadataFilename = sourceFile + "_metadata.json";
|
||||
Map<String, Serializable> actualMetadata = readMetadata(response.getBody().getInputStream());
|
||||
File actualMetadataFile = new File(metadataFilename);
|
||||
jsonObjectMapper.writerWithDefaultPrettyPrinter().writeValue(actualMetadataFile, actualMetadata);
|
||||
|
||||
Map<String, Serializable> expectedMetadata = readExpectedMetadata(metadataFilename, actualMetadataFile);
|
||||
assertEquals(expectedMetadata, actualMetadata,
|
||||
sourceFile+": The metadata did not match the expected value. It has been saved in "+actualMetadataFile.getAbsolutePath());
|
||||
actualMetadataFile.delete();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
fail(descriptor + " exception: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Serializable> readExpectedMetadata(String filename, File actualMetadataFile) throws IOException
|
||||
{
|
||||
try (InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(filename))
|
||||
{
|
||||
if (inputStream == null)
|
||||
{
|
||||
fail("The expected metadata file "+filename+" did not exist.\n"+
|
||||
"The actual metadata has been saved in "+actualMetadataFile.getAbsoluteFile());
|
||||
}
|
||||
return readMetadata(inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Serializable> readMetadata(InputStream inputStream) throws IOException
|
||||
{
|
||||
TypeReference<HashMap<String, Serializable>> typeRef = new TypeReference<HashMap<String, Serializable>>() {};
|
||||
return jsonObjectMapper.readValue(inputStream, typeRef);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2015-2022 Alfresco Software, Ltd. All rights reserved.
|
||||
*
|
||||
* License rights for this program may be obtained from Alfresco Software, Ltd.
|
||||
* pursuant to a written agreement and any use of this program without such an
|
||||
* agreement is prohibited.
|
||||
*/
|
||||
package org.alfresco.transform.base.registry;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import org.alfresco.transform.base.fakes.FakeTransformEngineWithTwoCustomTransformers;
|
||||
import org.alfresco.transform.base.fakes.FakeTransformerPdf2Png;
|
||||
import org.alfresco.transform.base.fakes.FakeTransformerTxT2Pdf;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.SpyBean;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.atLeast;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@AutoConfigureMockMvc
|
||||
@SpringBootTest(classes={org.alfresco.transform.base.Application.class}, properties={"transform.engine.config.cron=*/1 * * * * *"})
|
||||
@ContextConfiguration(classes = {
|
||||
FakeTransformEngineWithTwoCustomTransformers.class,
|
||||
FakeTransformerTxT2Pdf.class,
|
||||
FakeTransformerPdf2Png.class})
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
|
||||
public class TransformRegistryRefreshTest
|
||||
{
|
||||
@SpyBean
|
||||
private TransformRegistry transformRegistry;
|
||||
@Autowired
|
||||
private TransformConfigFromFiles transformConfigFromFiles;
|
||||
@Autowired
|
||||
private TransformConfigFiles transformConfigFiles;
|
||||
|
||||
@Test
|
||||
public void checkRegistryRefreshes() throws InterruptedException
|
||||
{
|
||||
waitForRegistryReady();
|
||||
assertEquals(4, transformRegistry.getTransformConfig().getTransformers().size());
|
||||
verify(transformRegistry, atLeast(1)).retrieveConfig();
|
||||
|
||||
// As we can't change the content of a classpath resource, lets change what is read.
|
||||
ReflectionTestUtils.setField(transformConfigFiles, "files", ImmutableMap.of(
|
||||
"a", "config/addA2B.json",
|
||||
"foo", "config/addB2C.json"));
|
||||
transformConfigFromFiles.initFileConfig();
|
||||
|
||||
Awaitility.await().pollDelay(3, TimeUnit.SECONDS).until( () -> { // i.e. Thread.sleep(3_000) - but keeps sona happy
|
||||
verify(transformRegistry, atLeast(1+2)).retrieveConfig();
|
||||
assertEquals(6, transformRegistry.getTransformConfig().getTransformers().size());
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void waitForRegistryReady() throws InterruptedException
|
||||
{
|
||||
Awaitility.await().atMost(1, TimeUnit.SECONDS)
|
||||
.pollInterval(100, TimeUnit.MILLISECONDS)
|
||||
.pollDelay(Duration.ZERO)
|
||||
.until(() -> transformRegistry.isReadyForTransformRequests());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
* #%L
|
||||
* Alfresco Transform Core
|
||||
* %%
|
||||
* Copyright (C) 2022 - 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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.registry;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import org.alfresco.transform.base.fakes.AbstractFakeTransformEngine;
|
||||
import org.alfresco.transform.base.fakes.FakeTransformEngineWithAllInOne;
|
||||
import org.alfresco.transform.base.fakes.FakeTransformEngineWithOneCustomTransformer;
|
||||
import org.alfresco.transform.base.fakes.FakeTransformEngineWithTwoCustomTransformers;
|
||||
import org.alfresco.transform.config.SupportedSourceAndTarget;
|
||||
import org.alfresco.transform.config.TransformConfig;
|
||||
import org.alfresco.transform.config.Transformer;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.alfresco.transform.common.Mimetype.MIMETYPE_EXCEL;
|
||||
import static org.alfresco.transform.common.Mimetype.MIMETYPE_PDF;
|
||||
import static org.alfresco.transform.common.Mimetype.MIMETYPE_WORD;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
|
||||
@AutoConfigureMockMvc
|
||||
@SpringBootTest(classes={org.alfresco.transform.base.Application.class})
|
||||
public class TransformRegistryTest
|
||||
{
|
||||
@Autowired
|
||||
private TransformRegistry transformRegistry;
|
||||
@Autowired
|
||||
private List<TransformConfigSource> transformConfigSources;
|
||||
@Autowired
|
||||
private TransformConfigFromTransformEngines transformConfigFromTransformEngines;
|
||||
@Autowired
|
||||
private TransformConfigFromFiles transformConfigFromFiles;
|
||||
@Autowired
|
||||
private TransformConfigFiles transformConfigFiles;
|
||||
@Autowired
|
||||
private TransformConfigFilesHistoric transformConfigFilesHistoric;
|
||||
|
||||
@AfterEach
|
||||
private void after()
|
||||
{
|
||||
transformConfigSources.clear();
|
||||
ReflectionTestUtils.setField(transformConfigFromTransformEngines, "transformEngines", Collections.emptyList());
|
||||
ReflectionTestUtils.setField(transformConfigFiles, "files", Collections.emptyMap());
|
||||
ReflectionTestUtils.setField(transformConfigFilesHistoric, "additional", Collections.emptyMap());
|
||||
ReflectionTestUtils.setField(transformRegistry, "isTRouter", false);
|
||||
transformRegistry.retrieveConfig();
|
||||
}
|
||||
|
||||
private String getTransformerNames(TransformConfig transformConfig)
|
||||
{
|
||||
return transformConfig.getTransformers().stream()
|
||||
.map(Transformer::getTransformerName)
|
||||
.sorted()
|
||||
.collect(Collectors.joining(", "));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noConfig()
|
||||
{
|
||||
assertEquals("", getTransformerNames(transformRegistry.getTransformConfig()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleTransformEngine()
|
||||
{
|
||||
ReflectionTestUtils.setField(transformConfigFromTransformEngines, "transformEngines", ImmutableList.of(
|
||||
new FakeTransformEngineWithOneCustomTransformer()));
|
||||
transformConfigFromTransformEngines.initTransformEngineConfig();
|
||||
transformRegistry.retrieveConfig();
|
||||
|
||||
assertEquals("Pdf2Jpg", getTransformerNames(transformRegistry.getTransformConfig()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleTransformEngines()
|
||||
{
|
||||
ReflectionTestUtils.setField(transformConfigFromTransformEngines, "transformEngines", ImmutableList.of(
|
||||
new FakeTransformEngineWithAllInOne(),
|
||||
new FakeTransformEngineWithOneCustomTransformer(),
|
||||
new FakeTransformEngineWithTwoCustomTransformers()));
|
||||
transformConfigFromTransformEngines.initTransformEngineConfig();
|
||||
transformRegistry.retrieveConfig();
|
||||
|
||||
assertEquals("Pdf2Jpg, Pdf2Png, TxT2Pdf, Txt2JpgViaPdf, Txt2PngViaPdf",
|
||||
getTransformerNames(transformRegistry.getTransformConfig()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uncombinedConfigFromEngine()
|
||||
{
|
||||
ReflectionTestUtils.setField(transformConfigFromTransformEngines, "transformEngines", ImmutableList.of(
|
||||
new FakeTransformEngineWithAllInOne(),
|
||||
new FakeTransformEngineWithTwoCustomTransformers()));
|
||||
transformConfigFromTransformEngines.initTransformEngineConfig();
|
||||
transformRegistry.retrieveConfig();
|
||||
|
||||
assertEquals("Pdf2Png, TxT2Pdf, Txt2JpgViaPdf, Txt2PngViaPdf",
|
||||
getTransformerNames(transformRegistry.getTransformConfig()));
|
||||
|
||||
ReflectionTestUtils.setField(transformRegistry, "isTRouter", true);
|
||||
transformConfigFromTransformEngines.initTransformEngineConfig();
|
||||
transformRegistry.retrieveConfig();
|
||||
|
||||
assertEquals("Pdf2Png, TxT2Pdf, Txt2PngViaPdf",
|
||||
getTransformerNames(transformRegistry.getTransformConfig()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void combinedConfigFromRouter()
|
||||
{
|
||||
ReflectionTestUtils.setField(transformRegistry, "isTRouter", true);
|
||||
ReflectionTestUtils.setField(transformConfigFromTransformEngines, "transformEngines", ImmutableList.of(
|
||||
new FakeTransformEngineWithAllInOne(),
|
||||
new FakeTransformEngineWithTwoCustomTransformers()));
|
||||
transformConfigFromTransformEngines.initTransformEngineConfig();
|
||||
transformRegistry.retrieveConfig();
|
||||
|
||||
assertEquals("Pdf2Png, TxT2Pdf, Txt2PngViaPdf",
|
||||
getTransformerNames(transformRegistry.getTransformConfig()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleTransformEngineWithAdditionalConfig()
|
||||
{
|
||||
ReflectionTestUtils.setField(transformConfigFromTransformEngines, "transformEngines", ImmutableList.of(
|
||||
new FakeTransformEngineWithOneCustomTransformer()));
|
||||
ReflectionTestUtils.setField(transformConfigFiles, "files", ImmutableMap.of(
|
||||
"a", "config/addA2B.json",
|
||||
"foo", "config/addB2C.json"));
|
||||
|
||||
transformConfigFromTransformEngines.initTransformEngineConfig();
|
||||
transformConfigFromFiles.initFileConfig();
|
||||
transformRegistry.retrieveConfig();
|
||||
|
||||
assertEquals("A2B, B2C, Pdf2Jpg", getTransformerNames(transformRegistry.getTransformConfig()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleTransformEngineWithHistoricAdditionalRoutes()
|
||||
{
|
||||
ReflectionTestUtils.setField(transformConfigFromTransformEngines, "transformEngines", ImmutableList.of(
|
||||
new FakeTransformEngineWithOneCustomTransformer()));
|
||||
ReflectionTestUtils.setField(transformConfigFilesHistoric, "additional", ImmutableMap.of(
|
||||
"a", "config/addA2B.json",
|
||||
"foo", "config/addB2C.json"));
|
||||
|
||||
transformConfigFromTransformEngines.initTransformEngineConfig();
|
||||
transformConfigFromFiles.initFileConfig();
|
||||
transformRegistry.retrieveConfig();
|
||||
|
||||
assertEquals("A2B, B2C, Pdf2Jpg", getTransformerNames(transformRegistry.getTransformConfig()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleTransformEngineWithHistoricTransformerRoutesExternalFile()
|
||||
{
|
||||
ReflectionTestUtils.setField(transformConfigFromTransformEngines, "transformEngines", ImmutableList.of(
|
||||
new FakeTransformEngineWithOneCustomTransformer()));
|
||||
ReflectionTestUtils.setField(transformConfigFilesHistoric, "TRANSFORMER_ROUTES_FROM_CLASSPATH",
|
||||
"config/removePdf2JpgAndAddA2Z.json"); // checking it is ignored
|
||||
ReflectionTestUtils.setField(transformConfigFilesHistoric, "transformerRoutesExternalFile",
|
||||
"config/addA2B.json");
|
||||
|
||||
transformConfigFromTransformEngines.initTransformEngineConfig();
|
||||
transformConfigFromFiles.initFileConfig();
|
||||
transformRegistry.retrieveConfig();
|
||||
|
||||
assertEquals("A2B, Pdf2Jpg", getTransformerNames(transformRegistry.getTransformConfig()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleTransformEngineWithHistoricTransformerRoutesOnClasspath()
|
||||
{
|
||||
ReflectionTestUtils.setField(transformConfigFromTransformEngines, "transformEngines", ImmutableList.of(
|
||||
new FakeTransformEngineWithOneCustomTransformer()));
|
||||
ReflectionTestUtils.setField(transformConfigFilesHistoric, "TRANSFORMER_ROUTES_FROM_CLASSPATH",
|
||||
"config/removePdf2JpgAndAddA2Z.json");
|
||||
|
||||
transformConfigFromTransformEngines.initTransformEngineConfig();
|
||||
transformConfigFromFiles.initFileConfig();
|
||||
transformRegistry.retrieveConfig();
|
||||
|
||||
assertEquals("A2Z", getTransformerNames(transformRegistry.getTransformConfig()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isReadyForTransformRequests()
|
||||
{
|
||||
transformConfigFromTransformEngines.initTransformEngineConfig();
|
||||
transformRegistry.retrieveConfig();
|
||||
assertFalse(transformRegistry.isReadyForTransformRequests());
|
||||
|
||||
ReflectionTestUtils.setField(transformConfigFromTransformEngines, "transformEngines", ImmutableList.of(
|
||||
new FakeTransformEngineWithOneCustomTransformer()));
|
||||
transformConfigFromTransformEngines.initTransformEngineConfig();
|
||||
transformRegistry.retrieveConfig();
|
||||
|
||||
assertTrue(transformRegistry.isReadyForTransformRequests());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckSourceSize()
|
||||
{
|
||||
ReflectionTestUtils.setField(transformConfigFromTransformEngines, "transformEngines", ImmutableList.of(
|
||||
new AbstractFakeTransformEngine()
|
||||
{
|
||||
@Override public TransformConfig getTransformConfig()
|
||||
{
|
||||
return TransformConfig.builder()
|
||||
.withTransformers(ImmutableList.of(
|
||||
Transformer.builder()
|
||||
.withTransformerName("transformerName")
|
||||
.withSupportedSourceAndTargetList(ImmutableSet.of(
|
||||
SupportedSourceAndTarget.builder()
|
||||
.withSourceMediaType(MIMETYPE_WORD)
|
||||
.withTargetMediaType(MIMETYPE_PDF)
|
||||
.build(),
|
||||
SupportedSourceAndTarget.builder()
|
||||
.withSourceMediaType(MIMETYPE_EXCEL)
|
||||
.withTargetMediaType(MIMETYPE_PDF)
|
||||
.withMaxSourceSizeBytes(12345L)
|
||||
.build()))
|
||||
.build()))
|
||||
.build();
|
||||
}
|
||||
}));
|
||||
transformConfigFromTransformEngines.initTransformEngineConfig();
|
||||
transformRegistry.retrieveConfig();
|
||||
|
||||
assertTrue( transformRegistry.checkSourceSize("transformerName", MIMETYPE_WORD, Long.MAX_VALUE, MIMETYPE_PDF));
|
||||
assertTrue( transformRegistry.checkSourceSize("transformerName", MIMETYPE_EXCEL, 12345L, MIMETYPE_PDF));
|
||||
assertFalse(transformRegistry.checkSourceSize("transformerName", MIMETYPE_EXCEL, 12346L, MIMETYPE_PDF));
|
||||
assertFalse(transformRegistry.checkSourceSize("transformerName", "doesNotExist", 12345L, MIMETYPE_PDF));
|
||||
assertFalse(transformRegistry.checkSourceSize("doesNotExist", MIMETYPE_WORD, 12345L, MIMETYPE_PDF));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* #%L
|
||||
* Alfresco Transform Core
|
||||
* %%
|
||||
* Copyright (C) 2022 - 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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.transform;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import org.alfresco.transform.base.fakes.FakeTransformEngineWithFragments;
|
||||
import org.alfresco.transform.base.fakes.FakeTransformerFragments;
|
||||
import org.alfresco.transform.base.messaging.TransformReplySender;
|
||||
import org.alfresco.transform.base.model.FileRefEntity;
|
||||
import org.alfresco.transform.base.model.FileRefResponse;
|
||||
import org.alfresco.transform.base.probes.ProbeTransform;
|
||||
import org.alfresco.transform.base.sfs.SharedFileStoreClient;
|
||||
import org.alfresco.transform.client.model.TransformReply;
|
||||
import org.alfresco.transform.client.model.TransformRequest;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
|
||||
import javax.jms.Destination;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.alfresco.transform.base.transform.StreamHandlerTest.read;
|
||||
import static org.alfresco.transform.common.Mimetype.MIMETYPE_IMAGE_JPEG;
|
||||
import static org.alfresco.transform.common.Mimetype.MIMETYPE_PDF;
|
||||
import static org.alfresco.transform.common.Mimetype.MIMETYPE_TEXT_PLAIN;
|
||||
import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_TRANSFORM;
|
||||
import static org.alfresco.transform.common.RequestParamMap.SOURCE_MIMETYPE;
|
||||
import static org.alfresco.transform.common.RequestParamMap.TARGET_MIMETYPE;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.http.HttpStatus.OK;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@AutoConfigureMockMvc
|
||||
@SpringBootTest(classes={org.alfresco.transform.base.Application.class})
|
||||
@ContextConfiguration(classes = {
|
||||
FakeTransformEngineWithFragments.class,
|
||||
FakeTransformerFragments.class})
|
||||
public class FragmentHandlerTest
|
||||
{
|
||||
@Autowired
|
||||
private TransformHandler transformHandler;
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@MockBean
|
||||
protected SharedFileStoreClient fakeSfsClient;
|
||||
@MockBean
|
||||
private TransformReplySender transformReplySender;
|
||||
@MockBean
|
||||
private ProbeTransform probeTransform;
|
||||
|
||||
private void assertFragments(String sourceText, String expectedError, List<String> expectedLines)
|
||||
{
|
||||
List<Pair<Destination, TransformReply>> replies = new ArrayList<>();
|
||||
List<String> lines = new ArrayList<>();
|
||||
|
||||
String sourceReference = UUID.randomUUID().toString();
|
||||
String targetReference = UUID.randomUUID().toString();
|
||||
|
||||
when(fakeSfsClient.retrieveFile(any()))
|
||||
.thenReturn(new ResponseEntity<>(new ByteArrayResource(sourceText.getBytes(StandardCharsets.UTF_8)),
|
||||
new HttpHeaders(), OK));
|
||||
|
||||
when(fakeSfsClient.saveFile(any()))
|
||||
.thenAnswer(invocation ->
|
||||
{
|
||||
lines.add(read(invocation.getArgument(0)));
|
||||
return new FileRefResponse(new FileRefEntity(targetReference));
|
||||
});
|
||||
|
||||
doAnswer(invocation ->
|
||||
{
|
||||
replies.add(Pair.of(invocation.getArgument(0), invocation.getArgument(1)));
|
||||
return null;
|
||||
}).when(transformReplySender).send(any(), any());
|
||||
|
||||
TransformRequest request = TransformRequest
|
||||
.builder()
|
||||
.withRequestId(UUID.randomUUID().toString())
|
||||
.withSourceMediaType(MIMETYPE_PDF)
|
||||
.withTargetMediaType(MIMETYPE_IMAGE_JPEG)
|
||||
.withTargetExtension("jpeg")
|
||||
.withSchema(1)
|
||||
.withClientData("ACS")
|
||||
.withSourceReference(sourceReference)
|
||||
.withSourceSize(32L)
|
||||
.withInternalContextForTransformEngineTests()
|
||||
.build();
|
||||
transformHandler.handleMessageRequest(request, Long.MAX_VALUE, null, probeTransform);
|
||||
|
||||
TransformReply lastReply = replies.get(replies.size() - 1).getRight();
|
||||
String errorDetails = lastReply.getErrorDetails();
|
||||
int status = lastReply.getStatus();
|
||||
if (expectedError == null)
|
||||
{
|
||||
assertNull(errorDetails);
|
||||
assertEquals(HttpStatus.CREATED.value(), status);
|
||||
}
|
||||
else
|
||||
{
|
||||
assertEquals("Transform failed - "+expectedError, errorDetails);
|
||||
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR.value(), status);
|
||||
}
|
||||
assertEquals(expectedLines, lines);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testErrorIfHttp() throws Exception
|
||||
{
|
||||
String expectedError = "Fragments may only be sent via message queues. This an http request";
|
||||
mockMvc.perform(
|
||||
MockMvcRequestBuilders.multipart(ENDPOINT_TRANSFORM)
|
||||
.file(new MockMultipartFile("file", null, MIMETYPE_TEXT_PLAIN,
|
||||
"Start".getBytes(StandardCharsets.UTF_8)))
|
||||
.param(SOURCE_MIMETYPE, MIMETYPE_PDF)
|
||||
.param(TARGET_MIMETYPE, MIMETYPE_IMAGE_JPEG))
|
||||
.andExpect(status().isInternalServerError())
|
||||
.andExpect(status().reason(containsString(expectedError)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithoutCallingRespondWithFragment()
|
||||
{
|
||||
assertFragments("WithoutFragments", null, ImmutableList.of("WithoutFragments"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSingleRespondWithFragmentCall()
|
||||
{
|
||||
assertFragments("Finished", null, ImmutableList.of("Finished"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleFragmentCallsWithFinished()
|
||||
{
|
||||
assertFragments("line1\nline2\nFinished", null,
|
||||
ImmutableList.of("line1", "line2", "Finished"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleFragmentsCallsWithoutFinish()
|
||||
{
|
||||
assertFragments("line1\nline2\nline3", null,
|
||||
ImmutableList.of("line1", "line2", "line3"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleFragmentsCallsWithoutSendingLastFragment()
|
||||
{
|
||||
assertFragments("line1\nline2\nline3\nIgnored", null,
|
||||
ImmutableList.of("line1", "line2", "line3"));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoFragments()
|
||||
{
|
||||
assertFragments("NullFinished", "No fragments were produced", ImmutableList.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEndTooEarlyUsingFinished()
|
||||
{
|
||||
assertFragments("line1\nFinished\nline3", "Final fragment already sent",
|
||||
ImmutableList.of("line1", "Finished"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEndTooEarlyUsingNull()
|
||||
{
|
||||
assertFragments("line1\nNull\nline3", "Final fragment already sent",
|
||||
ImmutableList.of("line1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFinishedAndNull()
|
||||
{
|
||||
// Able to just ignore the extra null call that request nothing
|
||||
assertFragments("line1\nFinished\nNull", null, ImmutableList.of("line1", "Finished"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullAndNull()
|
||||
{
|
||||
// Able to just ignore the extra null call that request nothing
|
||||
assertFragments("line1\nNull\nNull", null, ImmutableList.of("line1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullAndFinished()
|
||||
{
|
||||
assertFragments("line1\nNull\nFinished", "Final fragment already sent",
|
||||
ImmutableList.of("line1"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,579 @@
|
||||
/*
|
||||
* #%L
|
||||
* Alfresco Transform Core
|
||||
* %%
|
||||
* Copyright (C) 2022 - 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 <http://www.gnu.org/licenses/>.
|
||||
* #L%
|
||||
*/
|
||||
package org.alfresco.transform.base.transform;
|
||||
|
||||
import org.alfresco.transform.base.CustomTransformer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Tests {@link StreamHandler}, {@link TransformManagerImpl#createSourceFile()} and
|
||||
* {@link TransformManagerImpl#createTargetFile()} methods.
|
||||
*/
|
||||
public class StreamHandlerTest
|
||||
{
|
||||
public static final String ORIGINAL = "Original";
|
||||
public static final String CHANGE = " plus some change";
|
||||
public static final String EXPECTED = ORIGINAL+ CHANGE;
|
||||
|
||||
TransformManagerImpl transformManager = new TransformManagerImpl();
|
||||
@TempDir
|
||||
public File tempDir;
|
||||
|
||||
private InputStream getSourceInputStreamFromBytes()
|
||||
{
|
||||
return new ByteArrayInputStream(ORIGINAL.getBytes(StandardCharsets.ISO_8859_1));
|
||||
}
|
||||
|
||||
private OutputStream getOutputStreamToFile(File sourceFile) throws FileNotFoundException
|
||||
{
|
||||
return new BufferedOutputStream(new FileOutputStream(sourceFile));
|
||||
}
|
||||
|
||||
private InputStream getInputStreamFromFile(File sourceFile) throws FileNotFoundException
|
||||
{
|
||||
return new BufferedInputStream(new FileInputStream(sourceFile));
|
||||
}
|
||||
|
||||
private File tempFile() throws IOException
|
||||
{
|
||||
return File.createTempFile("temp_", null, tempDir);
|
||||
}
|
||||
|
||||
private static void write(File file, String text) throws IOException
|
||||
{
|
||||
try (OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(file)))
|
||||
{
|
||||
write(outputStream, text);
|
||||
}
|
||||
}
|
||||
|
||||
private static void write(OutputStream outputStream, String text) throws IOException
|
||||
{
|
||||
byte[] bytes = text.getBytes(StandardCharsets.ISO_8859_1);
|
||||
outputStream.write(bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
public static String read(File file) throws IOException
|
||||
{
|
||||
try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file)))
|
||||
{
|
||||
return read(inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
private static String read(InputStream inputStream) throws IOException
|
||||
{
|
||||
return new String(inputStream.readAllBytes(), StandardCharsets.ISO_8859_1);
|
||||
}
|
||||
|
||||
private static String read(ByteArrayOutputStream outputStream)
|
||||
{
|
||||
return outputStream.toString(StandardCharsets.ISO_8859_1);
|
||||
}
|
||||
|
||||
private void closeInputStreamWithoutException(InputStream inputStream)
|
||||
{
|
||||
if (inputStream != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
inputStream.close();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStartWithInputStream() throws Exception
|
||||
{
|
||||
try (InputStream inputStream = getSourceInputStreamFromBytes();
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream())
|
||||
{
|
||||
transformManager.setInputStream(inputStream);
|
||||
OutputStream outputStreamLengthRecorder = transformManager.setOutputStream(outputStream);
|
||||
|
||||
write(outputStreamLengthRecorder, read(inputStream)+CHANGE);
|
||||
|
||||
transformManager.copyTargetFileToOutputStream();
|
||||
transformManager.getOutputStream().close();
|
||||
closeInputStreamWithoutException(inputStream);
|
||||
Long outputLength = transformManager.getOutputLength();
|
||||
transformManager.deleteSourceFile();
|
||||
transformManager.deleteTargetFile();
|
||||
|
||||
assertEquals(EXPECTED, read(outputStream));
|
||||
assertEquals(EXPECTED.length(), outputLength);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStartWithInputStreamAndCallCreateSourceFile() throws Exception
|
||||
{
|
||||
try (InputStream inputStream = getSourceInputStreamFromBytes();
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream())
|
||||
{
|
||||
transformManager.setInputStream(inputStream);
|
||||
OutputStream outputStreamLengthRecorder = transformManager.setOutputStream(outputStream);
|
||||
|
||||
File sourceFileCreatedByTransform = transformManager.createSourceFile();
|
||||
assertTrue(sourceFileCreatedByTransform.exists());
|
||||
write(outputStreamLengthRecorder, read(sourceFileCreatedByTransform)+CHANGE);
|
||||
|
||||
transformManager.copyTargetFileToOutputStream();
|
||||
transformManager.getOutputStream().close();
|
||||
closeInputStreamWithoutException(inputStream);
|
||||
Long outputLength = transformManager.getOutputLength();
|
||||
transformManager.deleteSourceFile();
|
||||
transformManager.deleteTargetFile();
|
||||
|
||||
assertEquals(EXPECTED, read(outputStream));
|
||||
assertEquals(EXPECTED.length(), outputLength);
|
||||
assertFalse(sourceFileCreatedByTransform.exists());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStartWithSourceFile() throws Exception
|
||||
{
|
||||
File sourceFile = tempFile();
|
||||
write(sourceFile, ORIGINAL);
|
||||
transformManager.setSourceFile(sourceFile);
|
||||
|
||||
try (InputStream inputStream = new BufferedInputStream(new FileInputStream(sourceFile));
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream())
|
||||
{
|
||||
transformManager.setInputStream(inputStream);
|
||||
OutputStream outputStreamLengthRecorder = transformManager.setOutputStream(outputStream);
|
||||
|
||||
write(outputStreamLengthRecorder, read(inputStream)+CHANGE);
|
||||
|
||||
transformManager.copyTargetFileToOutputStream();
|
||||
closeInputStreamWithoutException(inputStream);
|
||||
transformManager.getOutputStream().close();
|
||||
Long outputLength = transformManager.getOutputLength();
|
||||
transformManager.deleteSourceFile();
|
||||
transformManager.deleteTargetFile();
|
||||
|
||||
assertEquals(EXPECTED, read(outputStream));
|
||||
assertEquals(EXPECTED.length(), outputLength);
|
||||
assertFalse(sourceFile.exists());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStartWithSourceFileAndCallCreateSourceFile() throws Exception
|
||||
{
|
||||
File sourceFile = tempFile();
|
||||
write(sourceFile, ORIGINAL);
|
||||
transformManager.setSourceFile(sourceFile);
|
||||
|
||||
try (InputStream inputStream = new BufferedInputStream(new FileInputStream(sourceFile));
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream())
|
||||
{
|
||||
transformManager.setInputStream(inputStream);
|
||||
OutputStream outputStreamLengthRecorder = transformManager.setOutputStream(outputStream);
|
||||
|
||||
File sourceFileCreatedByTransform = transformManager.createSourceFile();
|
||||
assertEquals(sourceFile, sourceFileCreatedByTransform);
|
||||
write(outputStreamLengthRecorder, read(sourceFileCreatedByTransform)+CHANGE);
|
||||
|
||||
transformManager.copyTargetFileToOutputStream();
|
||||
closeInputStreamWithoutException(inputStream);
|
||||
transformManager.getOutputStream().close();
|
||||
Long outputLength = transformManager.getOutputLength();
|
||||
transformManager.deleteSourceFile();
|
||||
transformManager.deleteTargetFile();
|
||||
|
||||
assertEquals(EXPECTED, read(outputStream));
|
||||
assertEquals(EXPECTED.length(), outputLength);
|
||||
assertFalse(sourceFile.exists());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStartWithOutputStream()
|
||||
{
|
||||
// This method exists so that we have a test for each input or output type. However, it contains no code
|
||||
// because it would be identical to the testStartWithInputStream method. Testing without both and input
|
||||
// and output would be far more complicated.
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStartWithOutputStreamAndCallCreateTargetFile() throws Exception
|
||||
{
|
||||
try (InputStream inputStream = getSourceInputStreamFromBytes();
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream())
|
||||
{
|
||||
transformManager.setInputStream(inputStream);
|
||||
transformManager.setOutputStream(outputStream);
|
||||
|
||||
File targetFileCreatedByTransform = transformManager.createTargetFile();
|
||||
assertTrue(targetFileCreatedByTransform.exists());
|
||||
write(targetFileCreatedByTransform, read(inputStream)+CHANGE);
|
||||
|
||||
transformManager.copyTargetFileToOutputStream();
|
||||
transformManager.getOutputStream().close();
|
||||
closeInputStreamWithoutException(inputStream);
|
||||
Long outputLength = transformManager.getOutputLength();
|
||||
transformManager.deleteSourceFile();
|
||||
transformManager.deleteTargetFile();
|
||||
|
||||
assertEquals(EXPECTED, read(outputStream));
|
||||
assertEquals(EXPECTED.length(), outputLength);
|
||||
assertFalse(targetFileCreatedByTransform.exists());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStartWithTargetFile() throws Exception
|
||||
{
|
||||
File targetFile = tempFile();
|
||||
transformManager.setTargetFile(targetFile);
|
||||
|
||||
try (InputStream inputStream = getSourceInputStreamFromBytes();
|
||||
OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(targetFile)))
|
||||
{
|
||||
transformManager.setInputStream(inputStream);
|
||||
OutputStream outputStreamLengthRecorder = transformManager.setOutputStream(outputStream);
|
||||
|
||||
write(outputStreamLengthRecorder, read(inputStream)+CHANGE);
|
||||
|
||||
transformManager.copyTargetFileToOutputStream();
|
||||
transformManager.getOutputStream().close();
|
||||
closeInputStreamWithoutException(inputStream);
|
||||
String actual = read(targetFile);
|
||||
Long outputLength = transformManager.getOutputLength();
|
||||
transformManager.deleteSourceFile();
|
||||
transformManager.deleteTargetFile();
|
||||
|
||||
assertEquals(EXPECTED, actual);
|
||||
assertEquals(EXPECTED.length(), outputLength);
|
||||
assertFalse(targetFile.exists());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStartWithTargetFileAndCallCreateTargetFile() throws Exception
|
||||
{
|
||||
File targetFile = tempFile();
|
||||
transformManager.setTargetFile(targetFile);
|
||||
|
||||
try (InputStream inputStream = getSourceInputStreamFromBytes();
|
||||
OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(targetFile)))
|
||||
{
|
||||
transformManager.setInputStream(inputStream);
|
||||
transformManager.setOutputStream(outputStream);
|
||||
|
||||
File targetFileCreatedByTransform = transformManager.createTargetFile();
|
||||
assertEquals(targetFile, targetFileCreatedByTransform);
|
||||
write(targetFileCreatedByTransform, read(inputStream)+CHANGE);
|
||||
|
||||
transformManager.copyTargetFileToOutputStream();
|
||||
transformManager.getOutputStream().close();
|
||||
closeInputStreamWithoutException(inputStream);
|
||||
String actual = read(targetFile);
|
||||
Long outputLength = transformManager.getOutputLength();
|
||||
transformManager.deleteSourceFile();
|
||||
transformManager.deleteTargetFile();
|
||||
|
||||
assertEquals(EXPECTED, actual);
|
||||
assertEquals(EXPECTED.length(), outputLength);
|
||||
assertFalse(targetFile.exists());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandleHttpRequestApproachUsingSourceAndTargetStreams()
|
||||
{
|
||||
// This method exists so that we have a test for each request approach. However, it contains no code
|
||||
// because it would be identical to the testStartWithInputStream method.
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandleProbeRequestApproachUsingSourceAndTargetFilesButKeepingTheTarget() throws Exception
|
||||
{
|
||||
File targetFile = tempFile();
|
||||
File sourceFile = tempFile();
|
||||
write(sourceFile, ORIGINAL);
|
||||
transformManager.setSourceFile(sourceFile);
|
||||
transformManager.keepTargetFile();
|
||||
|
||||
try (InputStream inputStream = new BufferedInputStream(new FileInputStream(sourceFile));
|
||||
OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(targetFile)))
|
||||
{
|
||||
transformManager.setInputStream(inputStream);
|
||||
OutputStream outputStreamLengthRecorder = transformManager.setOutputStream(outputStream);
|
||||
|
||||
write(outputStreamLengthRecorder, read(inputStream)+CHANGE);
|
||||
|
||||
transformManager.copyTargetFileToOutputStream();
|
||||
closeInputStreamWithoutException(inputStream);
|
||||
transformManager.getOutputStream().close();
|
||||
Long outputLength = transformManager.getOutputLength();
|
||||
transformManager.deleteSourceFile();
|
||||
transformManager.deleteTargetFile();
|
||||
|
||||
assertEquals(EXPECTED, read(targetFile));
|
||||
assertEquals(EXPECTED.length(), outputLength);
|
||||
assertFalse(sourceFile.exists());
|
||||
assertTrue(targetFile.exists());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandleMessageRequestApproachUsingSourceAndTargetFiles() throws Exception
|
||||
{
|
||||
File targetFile = tempFile();
|
||||
File sourceFile = tempFile();
|
||||
write(sourceFile, ORIGINAL);
|
||||
transformManager.setSourceFile(sourceFile);
|
||||
transformManager.setTargetFile(targetFile);
|
||||
|
||||
try (InputStream inputStream = new BufferedInputStream(new FileInputStream(sourceFile));
|
||||
OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(targetFile)))
|
||||
{
|
||||
transformManager.setInputStream(inputStream);
|
||||
OutputStream outputStreamLengthRecorder = transformManager.setOutputStream(outputStream);
|
||||
|
||||
write(outputStreamLengthRecorder, read(inputStream)+CHANGE);
|
||||
|
||||
transformManager.copyTargetFileToOutputStream();
|
||||
closeInputStreamWithoutException(inputStream);
|
||||
transformManager.getOutputStream().close();
|
||||
String actual = read(targetFile);
|
||||
Long outputLength = transformManager.getOutputLength();
|
||||
transformManager.deleteSourceFile();
|
||||
transformManager.deleteTargetFile();
|
||||
|
||||
assertEquals(EXPECTED, actual);
|
||||
assertEquals(EXPECTED.length(), outputLength);
|
||||
assertFalse(sourceFile.exists());
|
||||
assertFalse(targetFile.exists());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandleMessageRequestApproachUsingInputStreamAndTargetFile() throws Exception
|
||||
{
|
||||
File targetFile = tempFile();
|
||||
transformManager.setTargetFile(targetFile);
|
||||
|
||||
try (InputStream inputStream = getSourceInputStreamFromBytes();
|
||||
OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(targetFile)))
|
||||
{
|
||||
transformManager.setInputStream(inputStream);
|
||||
OutputStream outputStreamLengthRecorder = transformManager.setOutputStream(outputStream);
|
||||
|
||||
write(outputStreamLengthRecorder, read(inputStream)+CHANGE);
|
||||
|
||||
transformManager.copyTargetFileToOutputStream();
|
||||
closeInputStreamWithoutException(inputStream);
|
||||
transformManager.getOutputStream().close();
|
||||
String actual = read(targetFile);
|
||||
Long outputLength = transformManager.getOutputLength();
|
||||
transformManager.deleteSourceFile();
|
||||
transformManager.deleteTargetFile();
|
||||
|
||||
assertEquals(EXPECTED, actual);
|
||||
assertEquals(EXPECTED.length(), outputLength);
|
||||
assertFalse(targetFile.exists());
|
||||
}
|
||||
}
|
||||
|
||||
private abstract class FakeStreamHandler extends StreamHandler
|
||||
{
|
||||
@Override
|
||||
public void handleTransformRequest() throws Exception
|
||||
{
|
||||
init();
|
||||
handleTransform(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void transform(CustomTransformer customTransformer) throws Exception
|
||||
{
|
||||
write(outputStream, read(inputStream)+CHANGE);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimulatedHandleHttpRequest() throws Exception
|
||||
{
|
||||
File targetFile = tempFile();
|
||||
|
||||
try (ByteArrayOutputStream os = new ByteArrayOutputStream())
|
||||
{
|
||||
new FakeStreamHandler()
|
||||
{
|
||||
@Override
|
||||
protected void init() throws IOException
|
||||
{
|
||||
transformManager.setTargetFile(targetFile);
|
||||
transformManager.keepTargetFile();
|
||||
super.init();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InputStream getInputStream()
|
||||
{
|
||||
return getSourceInputStreamFromBytes();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected OutputStream getOutputStream()
|
||||
{
|
||||
return os;
|
||||
}
|
||||
}.handleTransformRequest();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimulatedHandleProbeRequest() throws Exception
|
||||
{
|
||||
File targetFile = tempFile();
|
||||
File sourceFile = tempFile();
|
||||
write(sourceFile, ORIGINAL);
|
||||
|
||||
new FakeStreamHandler()
|
||||
{
|
||||
@Override
|
||||
protected void init() throws IOException
|
||||
{
|
||||
transformManager.setSourceFile(sourceFile);
|
||||
transformManager.setTargetFile(targetFile);
|
||||
transformManager.keepTargetFile();
|
||||
super.init();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InputStream getInputStream() throws IOException
|
||||
{
|
||||
return getInputStreamFromFile(sourceFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected OutputStream getOutputStream() throws IOException
|
||||
{
|
||||
return getOutputStreamToFile(targetFile);
|
||||
}
|
||||
}.handleTransformRequest();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimulatedHandleMessageRequestUsingSharedFileStore() throws Exception
|
||||
{
|
||||
File targetFile = tempFile();
|
||||
File sourceFile = tempFile();
|
||||
write(sourceFile, ORIGINAL);
|
||||
|
||||
new FakeStreamHandler()
|
||||
{
|
||||
@Override
|
||||
protected InputStream getInputStream() throws IOException
|
||||
{
|
||||
return getInputStreamFromFile(sourceFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected OutputStream getOutputStream() throws IOException
|
||||
{
|
||||
return getOutputStreamToFile(targetFile);
|
||||
}
|
||||
}.handleTransformRequest();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimulatedHandleMessageRequestUsingDirectAccessUrl() throws Exception
|
||||
{
|
||||
File targetFile = tempFile();
|
||||
|
||||
new FakeStreamHandler()
|
||||
{
|
||||
@Override
|
||||
protected InputStream getInputStream()
|
||||
{
|
||||
return getSourceInputStreamFromBytes();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected OutputStream getOutputStream()
|
||||
throws FileNotFoundException
|
||||
{
|
||||
return getOutputStreamToFile(targetFile);
|
||||
}
|
||||
}.handleTransformRequest();
|
||||
}
|
||||
|
||||
@Test
|
||||
// Tried and failed to create TransformHandler.handleHttpRequest(...) that returned a
|
||||
// ResponseEntity<StreamingResponseBody> (and other async variants) so that we would not need a temporary target
|
||||
// file as a StreamingResponseBody would have allowed us to write directly to the OutputStream. However, I was
|
||||
// unable to find a way to defer setting the httpStatus in the response until we knew there were no Exceptions
|
||||
// thrown in processing. Keeping the following test (it does no harm) to show how much simpler it would have been.
|
||||
public void testSimulatedHandleHttpRequestWithStreamingResponseBody() throws Exception
|
||||
{
|
||||
try (ByteArrayOutputStream os = new ByteArrayOutputStream())
|
||||
{
|
||||
new FakeStreamHandler()
|
||||
{
|
||||
@Override
|
||||
protected InputStream getInputStream()
|
||||
{
|
||||
return getSourceInputStreamFromBytes();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected OutputStream getOutputStream()
|
||||
{
|
||||
return os;
|
||||
}
|
||||
}.handleTransformRequest();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
engines/base/src/test/resources/config/addA2B.json
Normal file
11
engines/base/src/test/resources/config/addA2B.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"transformers": [
|
||||
{
|
||||
"transformerName": "A2B",
|
||||
"supportedSourceAndTargetList": [
|
||||
{"sourceMediaType": "A", "targetMediaType": "B"}
|
||||
],
|
||||
"transformOptions": []
|
||||
}
|
||||
]
|
||||
}
|
||||
11
engines/base/src/test/resources/config/addB2C.json
Normal file
11
engines/base/src/test/resources/config/addB2C.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"transformers": [
|
||||
{
|
||||
"transformerName": "B2C",
|
||||
"supportedSourceAndTargetList": [
|
||||
{"sourceMediaType": "B", "targetMediaType": "C"}
|
||||
],
|
||||
"transformOptions": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"removeTransformers" : [
|
||||
"Pdf2Jpg"
|
||||
],
|
||||
"transformers": [
|
||||
{
|
||||
"transformerName": "A2Z",
|
||||
"supportedSourceAndTargetList": [
|
||||
{"sourceMediaType": "A", "targetMediaType": "Z"}
|
||||
],
|
||||
"transformOptions": []
|
||||
}
|
||||
]
|
||||
}
|
||||
13
engines/base/src/test/resources/logback.xml
Normal file
13
engines/base/src/test/resources/logback.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<layout class="ch.qos.logback.classic.PatternLayout">
|
||||
<Pattern>
|
||||
%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n
|
||||
</Pattern>
|
||||
</layout>
|
||||
</appender>
|
||||
<root level="error">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
</configuration>
|
||||
1
engines/base/src/test/resources/original.txt
Normal file
1
engines/base/src/test/resources/original.txt
Normal file
@@ -0,0 +1 @@
|
||||
Original Text
|
||||
Reference in New Issue
Block a user