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:
Alan Davis
2022-09-14 13:40:19 +01:00
committed by GitHub
parent ea83ef9ebc
commit babe26b0ba
652 changed files with 19479 additions and 18195 deletions

74
engines/aio/Dockerfile Normal file
View File

@@ -0,0 +1,74 @@
# Image provides an all-in-one (AIO) container in which to run core transformations for Alfresco Content Services.
# Tika is from Apache. See the license at http://www.apache.org/licenses/LICENSE-2.0.
# LibreOffice is from The Document Foundation. See the license at https://www.libreoffice.org/download/license/ or in /libreoffice.txt.
# ImageMagick is from ImageMagick Studio LLC. See the license at http://www.imagemagick.org/script/license.php or in /ImageMagick-license.txt.
# alfresco-pdf-renderer uses the PDFium library from Google Inc. See the license at https://pdfium.googlesource.com/pdfium/+/master/LICENSE or in /pdfium.txt.
FROM alfresco/alfresco-base-java:jre17-rockylinux8-202207110835
ARG EXIFTOOL_VERSION=12.25
ARG EXIFTOOL_FOLDER=Image-ExifTool-${EXIFTOOL_VERSION}
ARG EXIFTOOL_URL=https://nexus.alfresco.com/nexus/service/local/repositories/thirdparty/content/org/exiftool/image-exiftool/${EXIFTOOL_VERSION}/image-exiftool-${EXIFTOOL_VERSION}.tgz
ARG IMAGEMAGICK_VERSION=7.1.0-16
ENV IMAGEMAGICK_RPM_URL=https://github.com/Alfresco/imagemagick-build/releases/download/v${IMAGEMAGICK_VERSION}/ImageMagick-${IMAGEMAGICK_VERSION}.x86_64.rpm
ENV IMAGEMAGICK_LIB_RPM_URL=https://github.com/Alfresco/imagemagick-build/releases/download/v${IMAGEMAGICK_VERSION}/ImageMagick-libs-${IMAGEMAGICK_VERSION}.x86_64.rpm
ENV IMAGEMAGICK_DEP_RPM_URL=https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm
ARG LIBREOFFICE_VERSION=7.2.5
ENV LIBREOFFICE_RPM_URL=https://nexus.alfresco.com/nexus/service/local/repositories/thirdparty/content/org/libreoffice/libreoffice-dist/${LIBREOFFICE_VERSION}/libreoffice-dist-${LIBREOFFICE_VERSION}-linux.gz
ENV ALFRESCO_PDF_RENDERER_LIB_RPM_URL=https://nexus.alfresco.com/nexus/service/local/repositories/releases/content/org/alfresco/alfresco-pdf-renderer/1.1/alfresco-pdf-renderer-1.1-linux.tgz
ENV JAVA_OPTS=""
# Set default user information
ARG GROUPNAME=Alfresco
ARG GROUPID=1000
ARG AIOUSERNAME=transform-all-in-one
ARG USERID=33017
COPY target/${env.project_artifactId}-${env.project_version}.jar /usr/bin
#removing perl-ExtUtils-MakeMaker cascades to remove exiftools
RUN ln /usr/bin/${env.project_artifactId}-${env.project_version}.jar /usr/bin/${env.project_artifactId}.jar && \
yum install -y $IMAGEMAGICK_DEP_RPM_URL && \
yum install -y $IMAGEMAGICK_LIB_RPM_URL $IMAGEMAGICK_RPM_URL && \
rpm -e --nodeps libgs && \
yum install -y cairo cups-libs libSM libGLU && \
test -f libreoffice-dist-${LIBREOFFICE_VERSION}-linux.gz && \
ln -s libreoffice-dist-${LIBREOFFICE_VERSION}-linux.gz libreoffice-dist-linux.gz || \
curl -s -S $LIBREOFFICE_RPM_URL -o libreoffice-dist-linux.gz && \
tar xzf libreoffice-dist-linux.gz && \
yum localinstall -y LibreOffice*/RPMS/*.rpm && \
rm -rf libreoffice-dist-*linux.gz LibreOffice_*_Linux_x86-64_rpm && \
curl -s -S $ALFRESCO_PDF_RENDERER_LIB_RPM_URL -o alfresco-pdf-renderer-linux.tgz && \
tar xf alfresco-pdf-renderer-linux.tgz -C /usr/bin && \
rm -f alfresco-pdf-renderer-linux.tgz && \
curl -s -S $EXIFTOOL_URL -o ${EXIFTOOL_FOLDER}.tgz && \
tar xzf ${EXIFTOOL_FOLDER}.tgz && \
yum -y install perl perl-ExtUtils-MakeMaker make && \
(cd ./${EXIFTOOL_FOLDER} && \
perl Makefile.PL && \
make && \
make test && \
make install) && \
yum -y autoremove make && \
rm -rf ${EXIFTOOL_FOLDER} ${EXIFTOOL_FOLDER}.tgz && \
yum clean all
ADD target/generated-resources/licenses /licenses
ADD target/generated-resources/licenses.xml /licenses/
ADD target/generated-sources/license/THIRD-PARTY.txt /licenses/
COPY target/classes/licenses/3rd-party/ /
RUN groupadd -g ${GROUPID} ${GROUPNAME} && \
useradd -u ${USERID} -G ${GROUPNAME} ${AIOUSERNAME} && \
chgrp -R ${GROUPNAME} /usr/bin/${env.project_artifactId}.jar
EXPOSE 8090
USER ${AIOUSERNAME}
ENTRYPOINT java $JAVA_OPTS -jar /usr/bin/${env.project_artifactId}.jar

422
engines/aio/pom.xml Normal file
View File

@@ -0,0 +1,422 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>alfresco-transform-core-aio</artifactId>
<name>- All-In-One</name>
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-transform-core</artifactId>
<version>3.0.0-HXP-A10-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<properties>
<image.name>alfresco/alfresco-transform-core-aio</image.name>
<image.registry>quay.io</image.registry>
<env.project_artifactId>${project.artifactId}</env.project_artifactId>
</properties>
<dependencies>
<dependency>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-base-t-engine</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-base-t-engine</artifactId>
<version>${project.version}</version>
<classifier>tests</classifier>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>com.vaadin.external.google</groupId>
<artifactId>android-json</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
</dependency>
<dependency>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-jodconverter-core</artifactId>
<exclusions>
<exclusion>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
</dependency>
<dependency>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-transform-misc</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-transform-misc</artifactId>
<version>${project.version}</version>
<classifier>tests</classifier>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-transform-tika</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-transform-tika</artifactId>
<version>${project.version}</version>
<classifier>tests</classifier>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-transform-pdf-renderer</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-transform-pdf-renderer</artifactId>
<version>${project.version}</version>
<classifier>tests</classifier>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-transform-libreoffice</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-transform-libreoffice</artifactId>
<version>${project.version}</version>
<classifier>tests</classifier>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-transform-imagemagick</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-transform-imagemagick</artifactId>
<version>${project.version}</version>
<classifier>tests</classifier>
<type>test-jar</type>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Extract classes and licenses from fat Spring Boot jars. -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<includeGroupIds>${project.groupId}</includeGroupIds>
<includeArtifactIds>alfresco-transform-imagemagick,alfresco-transform-libreoffice,alfresco-transform-misc,alfresco-transform-pdf-renderer,alfresco-transform-tika</includeArtifactIds>
<excludeTransitive>true</excludeTransitive>
<excludeClassifiers>tests</excludeClassifiers>
<outputDirectory>${project.build.directory}</outputDirectory>
<outputAbsoluteArtifactFilename>true</outputAbsoluteArtifactFilename>
<overWriteReleases>true</overWriteReleases>
<overWriteSnapshots>true</overWriteSnapshots>
<fileMappers>
<org.codehaus.plexus.components.io.filemappers.RegExpFileMapper>
<pattern>BOOT-INF\/</pattern>
<replacement>./</replacement>
</org.codehaus.plexus.components.io.filemappers.RegExpFileMapper>
</fileMappers>
</configuration>
<executions>
<execution>
<id>unpack-classes</id>
<phase>generate-sources</phase>
<goals>
<goal>unpack-dependencies</goal>
</goals>
<configuration>
<includes>**/org/alfresco/transform/**/*.class</includes>
</configuration>
</execution>
<execution>
<id>unpack-resources</id>
<phase>generate-resources</phase>
<goals>
<goal>unpack-dependencies</goal>
</goals>
<configuration>
<excludes>**/*.class,BOOT-INF/lib/**,META-INF/**,org/**,**/*.idx,docker/**,**/application-default.yaml</excludes>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>org.alfresco.transform.base.Application</mainClass>
<excludes> <!-- Don't include source t-engine fat Spring jars. Required files have been extracted -->
<exclude>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-transform-imagemagick</artifactId>
</exclude>
<exclude>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-transform-libreoffice</artifactId>
</exclude>
<exclude>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-transform-misc</artifactId>
</exclude>
<exclude>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-transform-pdf-renderer</artifactId>
</exclude>
<exclude>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-transform-tika</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>license-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>docker-it-setup</id>
<!-- raises an ActiveMq container for the Integration Tests -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
</plugin>
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<executions>
<execution>
<id>integration-tests</id>
<goals>
<goal>start</goal>
<goal>stop</goal>
</goals>
<configuration>
<images>
<image>
<name>alfresco/alfresco-activemq:5.17.1-jre11-rockylinux8</name>
<run>
<hostname>activemq</hostname>
<ports>
<port>8161:8161</port>
<port>5672:5672</port>
<port>61616:61616</port>
</ports>
<wait>
<log>Apache ActiveMQ .* started</log>
<time>20000</time>
<kill>500</kill>
<shutdown>100</shutdown>
<exec>
<preStop>kill 1</preStop>
<preStop>kill -9 1</preStop>
</exec>
</wait>
</run>
</image>
<image>
<name>${image.name}:${image.tag}</name>
<run>
<ports>
<port>8090:8090</port>
</ports>
<wait>
<http>
<url>http://localhost:8090/transform/config</url>
<method>GET</method>
<status>200...299</status>
</http>
<time>300000</time>
<kill>500</kill>
<shutdown>100</shutdown>
<exec>
<preStop>kill 1</preStop>
<preStop>kill -9 1</preStop>
</exec>
</wait>
</run>
</image>
</images>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>local</id>
<build>
<plugins>
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<executions>
<execution>
<id>build-image</id>
<phase>package</phase>
<goals>
<goal>build</goal>
</goals>
<configuration>
<images>
<image>
<name>${image.name}:${image.tag}</name>
<build>
<contextDir>${project.basedir}</contextDir>
<buildOptions>
<squash>true</squash>
</buildOptions>
</build>
</image>
</images>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>internal</id>
<build>
<plugins>
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<configuration>
<images>
<!-- QuayIO image -->
<image>
<name>${image.name}:${image.tag}</name>
<registry>${image.registry}</registry>
<build>
<contextDir>${project.basedir}</contextDir>
<buildOptions>
<squash>true</squash>
</buildOptions>
</build>
</image>
<!-- DockerHub image -->
<image>
<name>${image.name}:${image.tag}</name>
<build>
<contextDir>${project.basedir}</contextDir>
<buildOptions>
<squash>true</squash>
</buildOptions>
</build>
</image>
</images>
</configuration>
<executions>
<execution>
<id>build-image</id>
<phase>package</phase>
<goals>
<goal>build</goal>
</goals>
</execution>
<execution>
<id>push-image</id>
<phase>install</phase>
<goals>
<goal>push</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>release</id>
<build>
<plugins>
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<configuration>
<images>
<!-- QuayIO image -->
<image>
<name>${image.name}:${project.version}</name>
<registry>${image.registry}</registry>
<build>
<contextDir>${project.basedir}</contextDir>
<buildOptions>
<squash>true</squash>
</buildOptions>
</build>
</image>
<!-- DockerHub image -->
<image>
<name>${image.name}:${project.version}</name>
<build>
<contextDir>${project.basedir}</contextDir>
<buildOptions>
<squash>true</squash>
</buildOptions>
</build>
</image>
</images>
</configuration>
<executions>
<execution>
<goals>
<goal>build</goal>
<goal>push</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@@ -0,0 +1,83 @@
/*
* #%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.coreaio;
import org.alfresco.transform.base.TransformEngine;
import org.alfresco.transform.base.probes.ProbeTransform;
import org.alfresco.transform.config.TransformConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import static org.alfresco.transform.base.logging.StandardMessages.COMMUNITY_LICENCE;
import static org.alfresco.transform.common.Mimetype.MIMETYPE_PDF;
import static org.alfresco.transform.common.Mimetype.MIMETYPE_TEXT_PLAIN;
@Component
public class AIOTransformEngine implements TransformEngine
{
@Autowired(required = false)
private List<TransformEngine> transformEngines;
@Override
public String getTransformEngineName()
{
return "0060 AllInOne";
}
@Override
public String getStartupMessage()
{
String message = "";
if (transformEngines != null)
{
// Combines the messages of the component TransformEngines. Removes duplicate community license messages.
message = transformEngines.stream()
.filter(transformEngine -> transformEngine != this)
.map(transformEngine -> transformEngine.getStartupMessage())
.collect( Collectors.joining("\n"));
message = message.replace(COMMUNITY_LICENCE, "");
}
return COMMUNITY_LICENCE + message;
}
@Override
public TransformConfig getTransformConfig()
{
return null;
}
@Override
public ProbeTransform getProbeTransform()
{
return new ProbeTransform("probe.pdf", MIMETYPE_PDF, MIMETYPE_TEXT_PLAIN, Collections.emptyMap(),
60, 16, 400, 10240, 60 * 30 + 1, 60 * 15 + 20);
}
}

View File

@@ -0,0 +1,23 @@
queue:
engineRequestQueue: ${TRANSFORM_ENGINE_REQUEST_QUEUE:org.alfresco.transform.engine.aio.acs}
transform:
core:
version: @project.version@
pdfrenderer:
exe: ${PDFRENDERER_EXE:/usr/bin/alfresco-pdf-renderer}
libreoffice:
path: ${LIBREOFFICE_HOME:/opt/libreoffice7.2}
maxTasksPerProcess: ${LIBREOFFICE_MAX_TASKS_PER_PROCESS:200}
timeout: ${LIBREOFFICE_TIMEOUT:1200000}
portNumbers: ${LIBREOFFICE_PORT_NUMBERS:8100}
templateProfileDir: ${LIBREOFFICE_TEMPLATE_PROFILE_DIR:}
isEnabled: ${LIBREOFFICE_IS_ENABLED:true}
imagemagick:
root: ${IMAGEMAGICK_ROOT:/usr/lib64/ImageMagick-7.0.10}
dyn: ${IMAGEMAGICK_DYN:/usr/lib64/ImageMagick-7.0.10/lib}
exe: ${IMAGEMAGICK_EXE:/usr/bin/convert}
coders: ${IMAGEMAGICK_CODERS:}
config: ${IMAGEMAGICK_CONFIG:}
tika:
pdfBox:
notExtractBookmarksTextDefault: ${PDFBOX_NOTEXTRACTBOOKMARKS_DEFAULT:false}

View File

@@ -0,0 +1,33 @@
/*
* #%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.aio;
import org.alfresco.transform.imagemagick.ImageMagickTransformationIT;
public class AIOImageMagickIT extends ImageMagickTransformationIT
{
}

View File

@@ -0,0 +1,36 @@
/*
* #%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.aio;
import org.alfresco.transform.imagemagick.ImageMagickTest;
/**
* Test ImageMagick functionality in All-In-One.
*/
public class AIOImageMagickTest extends ImageMagickTest
{
}

View File

@@ -0,0 +1,36 @@
/*
* #%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.aio;
import org.alfresco.transform.libreoffice.LibreOfficeTest;
/**
* Test LibreOffice functionality in All-In-One.
*/
public class AIOLibreOfficeTest extends LibreOfficeTest
{
}

View File

@@ -0,0 +1,33 @@
/*
* #%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.aio;
import org.alfresco.transform.libreoffice.LibreOfficeTransformationIT;
public class AIOLibreOfficeTransformationIT extends LibreOfficeTransformationIT
{
}

View File

@@ -0,0 +1,33 @@
/*
* #%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.aio;
import org.alfresco.transform.misc.MiscMetadataExtractsIT;
public class AIOMiscMetadataExtractsIT extends MiscMetadataExtractsIT
{
}

View File

@@ -0,0 +1,36 @@
/*
* #%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.aio;
import org.alfresco.transform.misc.MiscTest;
/**
* Test Misc functionality in All-In-One.
*/
public class AIOMiscTest extends MiscTest
{
}

View File

@@ -0,0 +1,33 @@
/*
* #%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.aio;
import org.alfresco.transform.misc.MiscTransformsIT;
public class AIOMiscTransformsIT extends MiscTransformsIT
{
}

View File

@@ -0,0 +1,33 @@
/*
* #%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.aio;
import org.alfresco.transform.pdfrenderer.PdfRendererTransformationIT;
public class AIOPdfRendererIT extends PdfRendererTransformationIT
{
}

View File

@@ -0,0 +1,36 @@
/*
* #%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.aio;
import org.alfresco.transform.pdfrenderer.PdfRendererTest;
/**
* Test PdfRenderer functionality in All-In-One.
*/
public class AIOPdfRendererTest extends PdfRendererTest
{
}

View File

@@ -0,0 +1,59 @@
/*
* #%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.aio;
import org.alfresco.transform.base.messaging.AbstractQueueIT;
import org.alfresco.transform.client.model.TransformRequest;
import java.util.UUID;
import static org.alfresco.transform.common.Mimetype.MIMETYPE_HTML;
import static org.alfresco.transform.common.Mimetype.MIMETYPE_TEXT_PLAIN;
/**
* @author David Edwards
* created on 21/04/2020
*/
public class AIOQueueIT extends AbstractQueueIT
{
@Override
protected TransformRequest buildRequest()
{
return TransformRequest
.builder()
.withRequestId(UUID.randomUUID().toString())
.withSourceMediaType(MIMETYPE_HTML)
.withTargetMediaType(MIMETYPE_TEXT_PLAIN)
.withTargetExtension("txt")
.withSchema(1)
.withClientData("ACS")
.withSourceReference(UUID.randomUUID().toString())
.withSourceSize(32L)
.withInternalContextForTransformEngineTests()
.build();
}
}

View File

@@ -0,0 +1,126 @@
/*
* #%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.aio;
import org.alfresco.transform.base.AbstractBaseTest;
import org.alfresco.transform.base.TransformController;
import org.alfresco.transform.config.TransformConfig;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import java.nio.file.Files;
import java.util.StringJoiner;
import static org.alfresco.transform.base.TransformControllerTest.getLogMessagesFor;
import static org.alfresco.transform.common.Mimetype.MIMETYPE_HTML;
import static org.alfresco.transform.common.Mimetype.MIMETYPE_TEXT_PLAIN;
import static org.alfresco.transform.common.RequestParamMap.CONFIG_VERSION_DEFAULT;
import static org.alfresco.transform.common.RequestParamMap.CONFIG_VERSION_LATEST;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
/**
* Test All-In-One.
*/
public class AIOTest extends AbstractBaseTest
{
@Autowired
private String coreVersion;
@BeforeEach
public void before() throws Exception
{
sourceMimetype = MIMETYPE_HTML;
targetMimetype = MIMETYPE_TEXT_PLAIN;
sourceExtension = "html";
targetExtension = "txt";
expectedOptions = null;
expectedSourceSuffix = null;
sourceFileBytes = readTestFile(sourceExtension);
expectedTargetFileBytes = Files.readAllBytes(getTestFile("quick2." + targetExtension, true).toPath());
sourceFile = new MockMultipartFile("file", "quick." + sourceExtension, sourceMimetype, sourceFileBytes);
}
@Override
// Add extra required parameters to the request.
protected MockHttpServletRequestBuilder mockMvcRequest(String url, MockMultipartFile sourceFile, String... params)
{
return super.mockMvcRequest(url, sourceFile, params)
.param("targetMimetype", targetMimetype)
.param("sourceMimetype", sourceMimetype);
}
@Test
public void coreVersionNotSetInOriginalConfigTest()
{
ResponseEntity<TransformConfig> responseEntity = controller.transformConfig(Integer.valueOf(CONFIG_VERSION_DEFAULT));
responseEntity.getBody().getTransformers().forEach(transformer -> {
assertNull(transformer.getCoreVersion(), transformer.getTransformerName() +
" should have had a null coreValue but was " + transformer.getCoreVersion());
});
}
@Test
public void coreVersionSetInLatestConfigTest()
{
ResponseEntity<TransformConfig> responseEntity = controller.transformConfig(CONFIG_VERSION_LATEST);
responseEntity.getBody().getTransformers().forEach(transformer -> {
assertNotNull(transformer.getCoreVersion(), transformer.getTransformerName() +
" should have had a coreValue but was null. Should have been " + coreVersion);
});
}
@Test
public void testStartupLogsIncludeEngineMessages()
{
StringJoiner controllerLogMessages = getLogMessagesFor(TransformController.class);
controller.startup();
assertEquals(
"--------------------------------------------------------------------------------------------------------------------------------------------------------------\n"
+ "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"
+ "This transformer uses ImageMagick from ImageMagick Studio LLC. See the license at http://www.imagemagick.org/script/license.php or in /ImageMagick-license.txt\n"
+ "This transformer uses LibreOffice from The Document Foundation. See the license at https://www.libreoffice.org/download/license/ or in /libreoffice.txt\n"
+ "This transformer uses libraries from Apache. See the license at http://www.apache.org/licenses/LICENSE-2.0. or in /Apache\\\\ 2.0.txt\n"
+ "This transformer uses htmlparser. See the license at http://htmlparser.sourceforge.net/license.html\n"
+ "This transformer uses alfresco-pdf-renderer which uses the PDFium library from Google Inc. See the license at https://pdfium.googlesource.com/pdfium/+/master/LICENSE or in /pdfium.txt\n"
+ "This transformer uses Tika from Apache. See the license at http://www.apache.org/licenses/LICENSE-2.0. or in /Apache\\ 2.0.txt\n"
+ "This transformer uses ExifTool by Phil Harvey. See license at https://exiftool.org/#license. or in /Perl-Artistic-License.txt\n"
+ "--------------------------------------------------------------------------------------------------------------------------------------------------------------\n"
+ "Starting application components... Done",
controllerLogMessages.toString());
}
}

View File

@@ -0,0 +1,33 @@
/*
* #%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.aio;
import org.alfresco.transform.tika.TikaMetadataExtractsIT;
public class AIOTikaMetadataExtractsIT extends TikaMetadataExtractsIT
{
}

View File

@@ -0,0 +1,75 @@
/*
* #%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.aio;
import com.google.common.collect.ImmutableSet;
import org.alfresco.transform.tika.TikaTest;
import org.junit.jupiter.api.Test;
import static org.alfresco.transform.base.html.OptionsHelper.getOptionNames;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Test Tika functionality in All-In-One.
*/
public class AIOTikaTest extends TikaTest
{
@Test
public void optionListTest()
{
assertEquals(ImmutableSet.of(
"allowEnlargement",
"allowPdfEnlargement",
"alphaRemove",
"autoOrient",
"commandOptions",
"cropGravity",
"cropHeight",
"cropPercentage",
"cropWidth",
"cropXOffset",
"cropYOffset",
"endPage",
"extractMapping",
"height",
"includeContents",
"maintainAspectRatio",
"maintainPdfAspectRatio",
"metadata",
"notExtractBookmarksText",
"page",
"pageLimit",
"resizeHeight",
"resizePercentage",
"resizeWidth",
"startPage",
"targetEncoding",
"thumbnail",
"width"),
getOptionNames(controller.transformConfig(0).getBody().getTransformOptions()));
}
}

View File

@@ -0,0 +1,33 @@
/*
* #%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.aio;
import org.alfresco.transform.tika.TikaTransformationIT;
public class AIOTikaTransformationIT extends TikaTransformationIT
{
}

View File

@@ -0,0 +1,999 @@
{
"transformOptions": {
"tikaOptions": [
{"value": {"name": "targetEncoding"}}
],
"archiveOptions": [
{"value": {"name": "includeContents"}},
{"value": {"name": "targetEncoding"}}
],
"pdfboxOptions": [
{"value": {"name": "notExtractBookmarksText"}},
{"value": {"name": "targetEncoding"}}
],
"metadataOptions": [
{"value": {"name": "extractMapping"}}
],
"metadataEmbedOptions": [
{"value": {"name": "metadata", "required": true}},
{"value": {"name": "targetEncoding"}}
]
},
"transformers": [
{
"transformerName": "Archive",
"supportedSourceAndTargetList": [
{"sourceMediaType": "application/x-cpio", "targetMediaType": "text/html"},
{"sourceMediaType": "application/x-cpio", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/x-cpio", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/x-cpio", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/java-archive", "targetMediaType": "text/html"},
{"sourceMediaType": "application/java-archive", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/java-archive", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/java-archive", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/x-tar", "targetMediaType": "text/html"},
{"sourceMediaType": "application/x-tar", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/x-tar", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/x-tar", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/zip", "targetMediaType": "text/html"},
{"sourceMediaType": "application/zip", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/zip", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/zip", "targetMediaType": "text/xml"}
],
"transformOptions": [
"archiveOptions"
]
},
{
"transformerName": "OutlookMsg",
"supportedSourceAndTargetList": [
{"sourceMediaType": "application/vnd.ms-outlook", "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-outlook", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-outlook", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-outlook", "targetMediaType": "text/xml"}
],
"transformOptions": [
"tikaOptions"
]
},
{
"transformerName": "PdfBox",
"supportedSourceAndTargetList": [
{"sourceMediaType": "application/pdf", "targetMediaType": "text/csv"},
{"sourceMediaType": "application/pdf", "targetMediaType": "text/html"},
{"sourceMediaType": "application/pdf", "maxSourceSizeBytes": 26214400, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/pdf", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/pdf", "targetMediaType": "text/xml"}
],
"transformOptions": [
"pdfboxOptions"
]
},
{
"transformerName": "Office",
"supportedSourceAndTargetList": [
{"sourceMediaType": "application/msword", "priority": 60, "targetMediaType": "text/html"},
{"sourceMediaType": "application/msword", "priority": 60, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/msword", "priority": 60, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/msword", "priority": 60, "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.ms-project", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-project", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-project", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-project", "priority": 55, "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.ms-outlook", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-outlook", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-outlook", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-outlook", "priority": 55, "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.ms-powerpoint", "priority": 60, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-powerpoint", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-powerpoint", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-powerpoint", "priority": 55, "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.visio", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.visio", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.visio", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.visio", "priority": 55, "targetMediaType": "text/xml"}
],
"transformOptions": [
"tikaOptions"
]
},
{
"transformerName": "Poi",
"supportedSourceAndTargetList": [
{"sourceMediaType": "application/vnd.ms-excel", "priority": 55, "targetMediaType": "text/csv"},
{"sourceMediaType": "application/vnd.ms-excel", "priority": 60, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-excel", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-excel", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-excel", "priority": 55, "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "priority": 55, "targetMediaType": "text/csv"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "priority": 65, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "priority": 60, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "priority": 60, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "priority": 60, "targetMediaType": "text/xml"}
],
"transformOptions": [
"tikaOptions"
]
},
{
"transformerName": "OOXML",
"supportedSourceAndTargetList": [
{"sourceMediaType": "application/vnd.ms-word.document.macroenabled.12", "priority": 60, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-word.document.macroenabled.12", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-word.document.macroenabled.12", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-word.document.macroenabled.12", "priority": 55, "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "priority": 60, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "priority": 55, "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.ms-word.template.macroenabled.12", "priority": 60, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-word.template.macroenabled.12", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-word.template.macroenabled.12", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-word.template.macroenabled.12", "priority": 55, "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.wordprocessingml.template", "priority": 60, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.wordprocessingml.template", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.wordprocessingml.template", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.wordprocessingml.template", "priority": 55, "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.ms-powerpoint.template.macroenabled.12", "priority": 60, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-powerpoint.template.macroenabled.12", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-powerpoint.template.macroenabled.12", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-powerpoint.template.macroenabled.12", "priority": 55, "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.template", "priority": 60, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.template", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.template", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.template", "priority": 55, "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.ms-powerpoint.addin.macroenabled.12", "priority": 60, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-powerpoint.addin.macroenabled.12", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-powerpoint.addin.macroenabled.12", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-powerpoint.addin.macroenabled.12", "priority": 55, "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.ms-powerpoint.slideshow.macroenabled.12", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-powerpoint.slideshow.macroenabled.12", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-powerpoint.slideshow.macroenabled.12", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-powerpoint.slideshow.macroenabled.12", "priority": 55, "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.slideshow", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.slideshow", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.slideshow", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.slideshow", "priority": 55, "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.ms-powerpoint.presentation.macroenabled.12", "priority": 60, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-powerpoint.presentation.macroenabled.12", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-powerpoint.presentation.macroenabled.12", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-powerpoint.presentation.macroenabled.12", "priority": 55, "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.presentation", "priority": 60, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.presentation", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.presentation", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.presentation", "priority": 55, "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.ms-powerpoint.slide.macroenabled.12", "priority": 60, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-powerpoint.slide.macroenabled.12", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-powerpoint.slide.macroenabled.12", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-powerpoint.slide.macroenabled.12", "priority": 55, "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.slide", "priority": 60, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.slide", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.slide", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.slide", "priority": 55, "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.ms-excel.addin.macroenabled.12", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-excel.addin.macroenabled.12", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-excel.addin.macroenabled.12", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-excel.addin.macroenabled.12", "priority": 55, "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.ms-excel.sheet.binary.macroenabled.12", "priority": 60, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-excel.sheet.binary.macroenabled.12", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-excel.sheet.binary.macroenabled.12", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-excel.sheet.binary.macroenabled.12", "priority": 55, "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.ms-excel.sheet.macroenabled.12", "priority": 60, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-excel.sheet.macroenabled.12", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-excel.sheet.macroenabled.12", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-excel.sheet.macroenabled.12", "priority": 55, "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "priority": 60, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "priority": 55, "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.ms-excel.template.macroenabled.12", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-excel.template.macroenabled.12", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-excel.template.macroenabled.12", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-excel.template.macroenabled.12", "priority": 55, "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.spreadsheetml.template", "priority": 60, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.spreadsheetml.template", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.spreadsheetml.template", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.spreadsheetml.template", "priority": 55, "targetMediaType": "text/xml"}
],
"transformOptions": [
"tikaOptions"
]
},
{
"transformerName": "TikaAuto",
"supportedSourceAndTargetList": [
{"sourceMediaType": "application/x-cpio", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/x-cpio", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/x-cpio", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/x-cpio", "priority": 55, "targetMediaType": "text/xml"},
{"sourceMediaType": "application/java-archive", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/java-archive", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/java-archive", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/java-archive", "priority": 55, "targetMediaType": "text/xml"},
{"sourceMediaType": "application/x-netcdf", "targetMediaType": "text/html"},
{"sourceMediaType": "application/x-netcdf", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/x-netcdf", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/x-netcdf", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/msword", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/msword", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/msword", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/msword", "priority": 55, "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.ms-word.document.macroenabled.12", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-word.document.macroenabled.12", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-word.document.macroenabled.12", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-word.document.macroenabled.12", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document" , "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.ms-word.template.macroenabled.12", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-word.template.macroenabled.12", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-word.template.macroenabled.12", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-word.template.macroenabled.12", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.wordprocessingml.template", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.wordprocessingml.template", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.wordprocessingml.template", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.wordprocessingml.template", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/x-gzip", "targetMediaType": "text/html"},
{"sourceMediaType": "application/x-gzip", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/x-gzip", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/x-gzip", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/x-hdf", "targetMediaType": "text/html"},
{"sourceMediaType": "application/x-hdf", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/x-hdf", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/x-hdf", "targetMediaType": "text/xml"},
{"sourceMediaType": "text/html", "targetMediaType": "text/html"},
{"sourceMediaType": "text/html", "priority": 60, "targetMediaType": "text/plain"},
{"sourceMediaType": "text/html", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "text/html", "targetMediaType": "text/xml"},
{"sourceMediaType": "text/x-java-source", "targetMediaType": "text/html"},
{"sourceMediaType": "text/x-java-source", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "text/x-java-source", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "text/x-java-source", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.apple.keynote", "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.apple.keynote", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.apple.keynote", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.apple.keynote", "priority": 120, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-project", "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-project", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-project", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-project", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.apple.numbers", "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.apple.numbers", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.apple.numbers", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.apple.numbers", "priority": 120, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.oasis.opendocument.chart", "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.oasis.opendocument.chart", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.oasis.opendocument.chart", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.oasis.opendocument.chart", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.oasis.opendocument.image", "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.oasis.opendocument.image", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.oasis.opendocument.image", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.oasis.opendocument.image", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.oasis.opendocument.text-master", "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.oasis.opendocument.text-master", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.oasis.opendocument.text-master", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.oasis.opendocument.text-master", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.oasis.opendocument.presentation", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.oasis.opendocument.presentation", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.oasis.opendocument.presentation", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.oasis.opendocument.presentation", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.oasis.opendocument.spreadsheet", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.oasis.opendocument.spreadsheet", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.oasis.opendocument.spreadsheet", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.oasis.opendocument.spreadsheet", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.oasis.opendocument.text", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.oasis.opendocument.text", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.oasis.opendocument.text", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.oasis.opendocument.text", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/ogg", "targetMediaType": "text/html"},
{"sourceMediaType": "application/ogg", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/ogg", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/ogg", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.oasis.opendocument.text-web", "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.oasis.opendocument.text-web", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.oasis.opendocument.text-web", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.oasis.opendocument.text-web", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.oasis.opendocument.presentation-template", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.oasis.opendocument.presentation-template", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.oasis.opendocument.presentation-template", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.oasis.opendocument.presentation-template", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.oasis.opendocument.spreadsheet-template", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.oasis.opendocument.spreadsheet-template", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.oasis.opendocument.spreadsheet-template", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.oasis.opendocument.spreadsheet-template", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.oasis.opendocument.text-template", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.oasis.opendocument.text-template", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.oasis.opendocument.text-template", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.oasis.opendocument.text-template", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.apple.pages", "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.apple.pages", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.apple.pages", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.apple.pages", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/pdf", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/pdf", "maxSourceSizeBytes": 26214400, "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/pdf", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/pdf", "priority": 55, "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.ms-powerpoint.template.macroenabled.12", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-powerpoint.template.macroenabled.12", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-powerpoint.template.macroenabled.12", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-powerpoint.template.macroenabled.12", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.template", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.template", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.template", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.template", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.ms-powerpoint.addin.macroenabled.12", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-powerpoint.addin.macroenabled.12", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-powerpoint.addin.macroenabled.12", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-powerpoint.addin.macroenabled.12", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.ms-powerpoint.slideshow.macroenabled.12", "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-powerpoint.slideshow.macroenabled.12", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-powerpoint.slideshow.macroenabled.12", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-powerpoint.slideshow.macroenabled.12", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.slideshow", "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.slideshow", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.slideshow", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.slideshow", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.ms-powerpoint", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-powerpoint", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-powerpoint", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-powerpoint", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.ms-powerpoint.presentation.macroenabled.12", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-powerpoint.presentation.macroenabled.12", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-powerpoint.presentation.macroenabled.12", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-powerpoint.presentation.macroenabled.12", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.presentation", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.presentation", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.presentation", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.presentation", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/x-rar-compressed", "targetMediaType": "text/html"},
{"sourceMediaType": "application/x-rar-compressed", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/x-rar-compressed", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/x-rar-compressed", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/rss+xml", "targetMediaType": "text/html"},
{"sourceMediaType": "application/rss+xml", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/rss+xml", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/rss+xml", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/rtf", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/rtf", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/rtf", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/rtf", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.ms-powerpoint.slide.macroenabled.12", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-powerpoint.slide.macroenabled.12", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-powerpoint.slide.macroenabled.12", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-powerpoint.slide.macroenabled.12", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.slide", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.slide", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.slide", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.slide", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.sun.xml.writer", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.sun.xml.writer", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.sun.xml.writer", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.sun.xml.writer", "targetMediaType": "text/xml"},
{"sourceMediaType": "text/plain", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "text/plain", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "text/plain", "priority": 55, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "text/plain", "priority": 55, "targetMediaType": "text/xml"},
{"sourceMediaType": "text/xml", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "text/xml", "priority": 55, "targetMediaType": "text/plain"},
{"sourceMediaType": "text/xml", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "text/xml", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.visio", "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.visio", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.visio", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.visio", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/xhtml+xml", "targetMediaType": "text/html"},
{"sourceMediaType": "application/xhtml+xml", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/xhtml+xml", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/xhtml+xml", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.ms-excel.addin.macroenabled.12", "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-excel.addin.macroenabled.12", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-excel.addin.macroenabled.12", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-excel.addin.macroenabled.12", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.ms-excel", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-excel", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-excel", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-excel", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.ms-excel.sheet.binary.macroenabled.12", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-excel.sheet.binary.macroenabled.12", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-excel.sheet.binary.macroenabled.12", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-excel.sheet.binary.macroenabled.12", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.ms-excel.sheet.macroenabled.12", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-excel.sheet.macroenabled.12", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-excel.sheet.macroenabled.12", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-excel.sheet.macroenabled.12", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.ms-excel.template.macroenabled.12", "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.ms-excel.template.macroenabled.12", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.ms-excel.template.macroenabled.12", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.ms-excel.template.macroenabled.12", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.spreadsheetml.template", "priority": 55, "targetMediaType": "text/html"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.spreadsheetml.template", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.spreadsheetml.template", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.spreadsheetml.template", "targetMediaType": "text/xml"},
{"sourceMediaType": "application/x-compress", "targetMediaType": "text/html"},
{"sourceMediaType": "application/x-compress", "targetMediaType": "text/plain"},
{"sourceMediaType": "application/x-compress", "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/x-compress", "targetMediaType": "text/xml"},
{"sourceMediaType": "text/csv", "priority": 120, "targetMediaType": "text/html"},
{"sourceMediaType": "text/csv", "priority": 120, "targetMediaType": "text/plain"},
{"sourceMediaType": "text/csv", "priority": 120, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "text/csv", "priority": 120, "targetMediaType": "text/xml"}
],
"transformOptions": [
"tikaOptions"
]
},
{
"transformerName": "TextMining",
"supportedSourceAndTargetList": [
{"sourceMediaType": "application/msword", "priority": 65, "targetMediaType": "text/html"},
{"sourceMediaType": "application/msword", "priority": 65, "targetMediaType": "text/plain"},
{"sourceMediaType": "application/msword", "priority": 65, "targetMediaType": "application/xhtml+xml"},
{"sourceMediaType": "application/msword", "targetMediaType": "text/xml"}
],
"transformOptions": [
"tikaOptions"
]
},
{
"transformerName": "DWGMetadataExtractor",
"supportedSourceAndTargetList": [
{"sourceMediaType": "application/dwg", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/vnd.dwg", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/x-dwg", "targetMediaType": "alfresco-metadata-extract"}
],
"transformOptions": [
"metadataOptions"
]
},
{
"transformerName": "MailMetadataExtractor",
"supportedSourceAndTargetList": [
{"sourceMediaType": "application/vnd.ms-outlook", "targetMediaType": "alfresco-metadata-extract"}
],
"transformOptions": [
"metadataOptions"
]
},
{
"transformerName": "MP3MetadataExtractor",
"supportedSourceAndTargetList": [
{"sourceMediaType": "audio/mpeg", "targetMediaType": "alfresco-metadata-extract"}
],
"transformOptions": [
"metadataOptions"
]
},
{
"transformerName": "OfficeMetadataExtractor",
"supportedSourceAndTargetList": [
{"sourceMediaType": "application/msword", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-excel", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-powerpoint", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.visio", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.visio2013", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-tika-msoffice-embedded; format=ole10_native", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-project", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-tika-msworks-spreadsheet", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-mspublisher", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-tika-msoffice", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/sldworks", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-tika-ooxml-protected", "targetMediaType": "alfresco-metadata-extract"}
],
"transformOptions": [
"metadataOptions"
]
},
{
"transformerName": "OpenDocumentMetadataExtractor",
"supportedSourceAndTargetList": [
{"sourceMediaType": "application/vnd.oasis.opendocument.text", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.oasis.opendocument.text-template", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.oasis.opendocument.graphics", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.oasis.opendocument.graphics-template", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.oasis.opendocument.presentation", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.oasis.opendocument.presentation-template", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.oasis.opendocument.spreadsheet", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.oasis.opendocument.spreadsheet-template", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.oasis.opendocument.chart", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.oasis.opendocument.chart-template", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.oasis.opendocument.image", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.oasis.opendocument.image-template", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.oasis.opendocument.formula", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.oasis.opendocument.formula-template", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.oasis.opendocument.text-master", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.oasis.opendocument.text-web", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.oasis.opendocument.database", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-vnd.oasis.opendocument.presentation", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "applicationvnd.oasis.opendocument.image-template", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-vnd.oasis.opendocument.text-web", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-vnd.oasis.opendocument.image", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-vnd.oasis.opendocument.spreadsheet-template", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.sun.xml.writer", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-vnd.oasis.opendocument.graphics-template", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-vnd.oasis.opendocument.chart", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-vnd.oasis.opendocument.spreadsheet", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-vnd.oasis.opendocument.text", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-vnd.oasis.opendocument.text-template", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-vnd.oasis.opendocument.formula", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-vnd.oasis.opendocument.image-template", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-vnd.oasis.opendocument.presentation-template", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "applicationvnd.oasis.opendocument.formula-template", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-vnd.oasis.opendocument.chart-template", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-vnd.oasis.opendocument.formula-template", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-vnd.oasis.opendocument.text-master", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "applicationvnd.oasis.opendocument.chart-template", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-vnd.oasis.opendocument.graphics", "targetMediaType": "alfresco-metadata-extract"}
],
"transformOptions": [
"metadataOptions"
]
},
{
"transformerName": "PdfBoxMetadataExtractor",
"supportedSourceAndTargetList": [
{"sourceMediaType": "application/pdf", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/illustrator", "targetMediaType": "alfresco-metadata-extract"}
],
"transformOptions": [
"metadataOptions"
]
},
{
"transformerName": "PoiMetadataExtractor",
"supportedSourceAndTargetList": [
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.presentation", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-powerpoint.template.macroenabled.12", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-excel.addin.macroenabled.12", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.wordprocessingml.template", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-excel.sheet.binary.macroenabled.12", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-powerpoint.slide.macroenabled.12", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-visio.drawing", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-powerpoint.slideshow.macroenabled.12", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-powerpoint.presentation.macroenabled.12", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.slide", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-excel.sheet.macroenabled.12", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-word.template.macroenabled.12", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-word.document.macroenabled.12", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-powerpoint.addin.macroenabled.12", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.spreadsheetml.template", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-xpsdocument", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-visio.drawing.macroenabled.12", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-visio.template.macroenabled.12", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "model/vnd.dwfx+xps", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.template", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-visio.stencil", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-visio.template", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.presentationml.slideshow", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-visio.stencil.macroenabled.12", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-excel.template.macroenabled.12", "targetMediaType": "alfresco-metadata-extract"}
],
"transformOptions": [
"metadataOptions"
]
},
{
"transformerName": "TikaAudioMetadataExtractor",
"supportedSourceAndTargetList": [
{"sourceMediaType": "video/x-m4v", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "audio/x-oggflac", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/mp4", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "audio/vorbis", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "video/3gpp", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "audio/x-flac", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "video/3gpp2", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "video/quicktime", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "audio/mp4", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "video/mp4", "targetMediaType": "alfresco-metadata-extract"}
],
"transformOptions": [
"metadataOptions"
]
},
{
"transformerName": "TikaAutoMetadataExtractor",
"supportedSourceAndTargetList": [
{"sourceMediaType": "application/vnd.ms-htmlhelp", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/atom+xml", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "audio/midi", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/aaigrid", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-bag", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.apple.keynote", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-quattro-pro; version=9", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-ibooks+zip", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "audio/wave", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-midi", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "text/xml", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/rss+xml", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-netcdf", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "video/x-daala", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/matlab-mat", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "audio/aiff", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/jaxa-pal-sar", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/x-pcraster", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/arg", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-kro", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/x-hdf5-image", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "audio/speex", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/big-gif", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/zlib", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-cosar", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-ntv2", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-archive", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/java-archive", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-vnd.sun.xml.writer", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-gmt", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-xml", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/gzip-compressed", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/ida", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "text/x-groovy", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/x-emf", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-rar", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/sar-ceos", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/acad", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/zip", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/vnd.adobe.photoshop", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-sharedlib", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "audio/x-m4a", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/webp", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.wap.xhtml+xml", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "audio/x-aiff", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-spreadsheetml", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/x-airsar", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-pcidsk", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-java-pack200", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/x-fujibas", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-zmap", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/x-bmp", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/bpg", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/rtf", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-xz", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-speex", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "audio/ogg; codecs=speex", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-l1b", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-gsbg", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-sdat", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-visio", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-coredump", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-msaccess", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-dods", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/png", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-outlook-pst", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/bsb", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-cpio", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "audio/ogg", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-tar", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-dbf", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "video/x-ogm", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-los-las", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/autocad_dwg", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-excel.workspace.3", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-excel.workspace.4", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/x-bpg", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "gzip/document", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "text/x-java", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-brotli", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/elas", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/x-jb2", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-cappi", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/epub+zip", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-ace2", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-sas-data", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-hdf", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/x-mff", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/x-srp", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/bmp", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "video/x-ogguvs", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "drawing/dwg", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-doq2", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-acad", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-kml", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-autocad", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/x-mff2", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-snodas", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/terragen", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-wcs", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "text/x-c++src", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/timestamped-data", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/tiff", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/msexcel", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-asp", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-rar-compressed", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-envi-hdr", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "text/iso19139+xml", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-tnef", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-ecrg-toc", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/aig", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "audio/x-wav", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/emf", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-bzip", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/jdem", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-webp", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-arj", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-lzma", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-java-vm", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/envisat", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-doq1", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "audio/vnd.wave", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-ppi", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/ilwis", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-gunzip", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/x-icon", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/ogg", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/svg+xml", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-ms-owner", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-grib", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/ms-tnef", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/fits", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "audio/x-mpeg", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-bzip2", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "text/tsv", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-fictionbook+xml", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-p-aux", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-font-ttf", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/x-xcf", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/x-ms-bmp", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/wmf", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/eir", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-matlab-data", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/deflate64", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "audio/wav", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-rs2", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-word", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-tsx", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-lcp", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-mbtiles", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "audio/x-oggpcm", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-epsilon", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-msgn", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "text/csv", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/x-dimap", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/vnd.microsoft.icon", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-envi", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-dwg", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.apple.numbers", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-word2006ml", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-bt", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-font-adobe-metric", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-rst", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vrt", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-ctg", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-e00-grid", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "audio/x-ogg-flac", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-compress", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/x-psd", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "text/rss", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/sdts-raster", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/oxps", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/leveller", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-ingr", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/sgi", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-pnm", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/raster", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "audio/x-ogg-pcm", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "audio/ogg; codecs=opus", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/fits", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-r", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/gif", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/java-vm", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/mspowerpoint", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-http", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-rmf", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-ogg", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "video/ogg", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/applefile", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "text/rtf", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/adrg", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "video/x-ogg-rgb", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-ngs-geoid", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-map", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/ceos", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/xpm", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-ers", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "video/x-ogg-yuv", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-isis2", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-nwt-grd", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-isis3", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-nwt-grc", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "video/daala", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-blx", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-tnef", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "video/x-dirac", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-ndf", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/vnd.wap.wbmp", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "video/theora", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/kate", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/pkcs7-mime", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/fit", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-ctable2", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-executable", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-isatab", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/grass-ascii-grid", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "text/plain", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/gzipped", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-gxf", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-cpg", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-lan", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-xyz", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.apple.pages", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/x-jbig2", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/nitf", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/mbox", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/chm", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-fast", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-gsc", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-deflate", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-grib2", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/x-ozi", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-pds", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.apple.iwork", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-usgs-dem", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-excel.sheet.2", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-excel.sheet.3", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/dif+xml", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-excel.sheet.4", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-java", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/geotiff", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-gsag", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-snappy", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "video/x-theora", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/ntf", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-pdf", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/xml", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.wordperfect; version=6.x", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/pkcs7-signature", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.wordperfect; version=5.1", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.wordperfect; version=5.0", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-arj-compressed", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/geotopic", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "text/x-java-source", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "audio/basic", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/pcisdk", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-rik", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "audio/opus", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/jp2", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-gtx", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-object", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/vnd.ms-wordml", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/x-wmf", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-rpf-toc", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-srtmhgt", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-generic-bin", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "text/vnd.iptc.anpa", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-msmetafile", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-wms", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "video/x-oggrgb", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/xcf", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/photoshop", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-lz4", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-7z-compressed", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/gff", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "video/x-oggyuv", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-msdownload", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/jpeg", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/icns", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-emf", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-geo-pdf", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "video/x-ogg-uvs", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "video/x-flv", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-zip-compressed", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/gzip", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-tika-unix-dump", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-coasp", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-dipex", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-til", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-gzip", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-gs7bg", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-unix-archive", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-elf", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/dted", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-rasterlite", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "audio/x-mp4a", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-gzip-compressed", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "application/x-chm", "targetMediaType": "alfresco-metadata-extract"},
{"sourceMediaType": "image/hfa", "targetMediaType": "alfresco-metadata-extract"}
],
"transformOptions": [
"metadataOptions"
]
},
{
"transformerName": "PoiMetadataEmbedder",
"supportedSourceAndTargetList": [
{"sourceMediaType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "targetMediaType": "alfresco-metadata-embed"}
],
"transformOptions": [
"metadataEmbedOptions"
]
}
]
}

161
engines/base/README.md Normal file
View File

@@ -0,0 +1,161 @@
# Common base code for T-Engines
This project provides a common base for T-Engines and supersedes the
[original base](https://github.com/Alfresco/alfresco-transform-core/blob/master/deprecated/alfresco-transformer-base).
This project provides a base Spring Boot application (as a jar) to which transform
specific code may be added. It includes actions such as communication between
components and logging.
For more details on build a custom T-Engine and T-Config, please refer to the docs in ACS Packaging, including:
* [ATS Configuration](https://github.com/Alfresco/acs-packaging/blob/master/docs/custom-transforms-and-renditions.md#ats-configuration)
* [Creating a T-Engine](https://github.com/Alfresco/acs-packaging/blob/master/docs/creating-a-t-engine.md)
## Overview
A T-Engine project which extends this base is expected to provide the following:
* An implementation of the [TransformEngine](https://github.com/Alfresco/alfresco-transform-core/blob/master/engines/base/src/main/java/org/alfresco/transform/base/TransformEngine.java)
interface to describe the T-Engine.
* Implementations of the [CustomTransformer](engines/base/src/main/java/org/alfresco/transform/base/CustomTransformer.java)
interface with the actual transform code.
* An `application-default.yaml` file to define a unique name for the message queue to the T-Engine.
The `TransformEngine` and `CustomTransformer` implementations should have an
`@Component` annotation and be in or below the`org.alfresco.transform` package, so
that they will be discovered by the base T-Engine.
The `TransformEngine.getTransformConfig()` method typically reads a `json` file.
The names in the config should match the names returned by the `CustomTransformer`
implementations.
**Example TransformEngine**
The `TransformEngineName` is important if the config from multiple T-Engines is being
combined as they are sorted by name.
```java
package org.alfresco.transform.example;
import com.google.common.collect.ImmutableMap;
import org.alfresco.transform.base.TransformEngine;
import org.alfresco.transform.base.probes.ProbeTransform;
import org.alfresco.transform.config.reader.TransformConfigResourceReader;
import org.alfresco.transform.config.TransformConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class HelloTransformEngine implements TransformEngine
{
@Autowired
private TransformConfigResourceReader transformConfigResourceReader;
@Override
public String getTransformEngineName()
{
return "0200_hello";
}
@Override
public String getStartupMessage()
{
return "Startup "+getTransformEngineName()+"\nNo 3rd party licenses";
}
@Override
public TransformConfig getTransformConfig()
{
return transformConfigResourceReader.read("classpath:hello_engine_config.json");
}
@Override
public ProbeTransform getProbeTransform()
{
return new ProbeTransform("probe.txt", "text/plain", "text/plain",
ImmutableMap.of("sourceEncoding", "UTF-8", "language", "English"),
11, 10, 150, 1024, 1, 60 * 2);
}
}
```
**Example CustomTransformer**
```java
package org.alfresco.transform.example;
import org.alfresco.transform.base.CustomTransformer;
import org.alfresco.transform.base.TransformManager;
import org.springframework.stereotype.Component;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map;
@Component
public class HelloTransformer implements CustomTransformer
{
@Override
public String getTransformerName()
{
return "hello";
}
@Override
public void transform(String sourceMimetype, InputStream inputStream, String targetMimetype,
OutputStream outputStream, Map<String, String> transformOptions, TransformManager transformManager)
throws Exception
{
String name = new String(inputStream.readAllBytes(), transformOptions.get("sourceEncoding"));
String greeting = String.format(getGreeting(transformOptions.get("language")), name);
byte[] bytes = greeting.getBytes(transformOptions.get("sourceEncoding"));
outputStream.write(bytes, 0, bytes.length);
}
private String getGreeting(String language)
{
return "Hello %s";
}
}
```
**Example T-Config** `resources/hello_engine_config.json`
```json
{
"transformOptions": {
"helloOptions": [
{"value": {"name": "language"}},
{"value": {"name": "sourceEncoding"}}
]
},
"transformers": [
{
"transformerName": "hello",
"supportedSourceAndTargetList": [
{"sourceMediaType": "text/plain", "targetMediaType": "text/plain" }
],
"transformOptions": [
"helloOptions"
]
}
]
}
```
**Example properties** `resources/application-default.yaml`
As can be seen the following defines a default which can be overridden by an environment variable.
```yaml
queue:
engineRequestQueue: ${TRANSFORM_ENGINE_REQUEST_QUEUE:org.alfresco.transform.engine.libreoffice.acs}
```
**Example ProbeTransform test file** `resources/probe.txt`
```text
Jane
```

130
engines/base/pom.xml Normal file
View File

@@ -0,0 +1,130 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>alfresco-base-t-engine</artifactId>
<name>- Base</name>
<parent>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-transform-core</artifactId>
<version>3.0.0-HXP-A10-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<properties>
<transformer.base.deploy.skip>false</transformer.base.deploy.skip>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>com.vaadin.external.google</groupId>
<artifactId>android-json</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
</dependency>
<dependency>
<groupId>org.alfresco</groupId>
<artifactId>alfresco-transform-model</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>org.messaginghub</groupId>
<artifactId>pooled-jms</artifactId>
</dependency>
<dependency>
<groupId>com.google.collections</groupId>
<artifactId>google-collections</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>4.2.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>test-jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>${transformer.base.deploy.skip}</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

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

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

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

View File

@@ -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;
}

View File

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

View File

@@ -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");
}
}
}

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

View File

@@ -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 + ")";
}
}
}

View File

@@ -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;
}

View File

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

View File

@@ -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;
}
}
}

View File

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

View File

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

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

View File

@@ -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";
}

View File

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

View File

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

View 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.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");
}
}
}

View File

@@ -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;
}
}
}

View File

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

View File

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

View File

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

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

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

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

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

View File

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

View File

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

View File

@@ -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;
}

View File

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

View 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.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);
}
}

View 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

View 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>

View 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>

View 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>

View File

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

View File

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

View File

@@ -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:")));
}
}

View File

@@ -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;
}
}

View File

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

View File

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

View File

@@ -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)
{
}
}
}

View File

@@ -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;
}
}
}

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

View File

@@ -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;
}
}

View File

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

View File

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

View File

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

View File

@@ -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
}
}

View File

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

View File

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

View File

@@ -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
{
}

View File

@@ -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
{
}

View File

@@ -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
{
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 (&lt;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>
* &#64;ParameterizedTest
*
* &#64;MethodSource("engineTransformations")
*
* &#64;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);
}
}

View File

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

View File

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

View File

@@ -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"));
}
}

View File

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

View File

@@ -0,0 +1,11 @@
{
"transformers": [
{
"transformerName": "A2B",
"supportedSourceAndTargetList": [
{"sourceMediaType": "A", "targetMediaType": "B"}
],
"transformOptions": []
}
]
}

View File

@@ -0,0 +1,11 @@
{
"transformers": [
{
"transformerName": "B2C",
"supportedSourceAndTargetList": [
{"sourceMediaType": "B", "targetMediaType": "C"}
],
"transformOptions": []
}
]
}

View File

@@ -0,0 +1,14 @@
{
"removeTransformers" : [
"Pdf2Jpg"
],
"transformers": [
{
"transformerName": "A2Z",
"supportedSourceAndTargetList": [
{"sourceMediaType": "A", "targetMediaType": "Z"}
],
"transformOptions": []
}
]
}

View 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>

Some files were not shown because too many files have changed in this diff Show More