mirror of
https://github.com/Alfresco/alfresco-transform-core.git
synced 2025-08-14 17:58:27 +00:00
Save point: [skip ci]
* example HelloTransformer
This commit is contained in:
@@ -78,8 +78,7 @@ public class AIOTransformEngine implements TransformEngine
|
||||
@Override
|
||||
public ProbeTransform getProbeTransform()
|
||||
{
|
||||
return new ProbeTransform("quick.pdf", "quick.txt",
|
||||
MIMETYPE_PDF, MIMETYPE_TEXT_PLAIN, Collections.emptyMap(),
|
||||
return new ProbeTransform("quick.pdf", MIMETYPE_PDF, MIMETYPE_TEXT_PLAIN, Collections.emptyMap(),
|
||||
60, 16, 400, 10240, 60 * 30 + 1, 60 * 15 + 20);
|
||||
}
|
||||
}
|
||||
|
@@ -1,233 +1,147 @@
|
||||
# Common code for Transform Engines
|
||||
# Common base code for T-Engines
|
||||
|
||||
This project contains code that is common between all the ACS T-Engine transformers that run as Spring Boot process (optionally within their own
|
||||
Docker containers). It performs common actions such as logging, throttling requests and handling the streaming of content to and from the container.
|
||||
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).
|
||||
|
||||
For more details on build a custom T-Engine, please refer to the current docs in ACS Packaging, including:
|
||||
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 transformer project is expected to provide the following files:
|
||||
A T-Engine project which extends this base is expected to provide the following:
|
||||
|
||||
~~~
|
||||
src/main/resources/templates/test.html
|
||||
src/main/java/org/alfresco/transformer/<TransformerName>Controller.java
|
||||
src/main/java/org/alfresco/transformer/Application.java
|
||||
~~~
|
||||
* 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.
|
||||
|
||||
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.
|
||||
|
||||
* test.html - A simple test page using [thymeleaf](http://www.thymeleaf.org) that gathers request
|
||||
parameters, so they may be used to test the transformer.
|
||||
The `TransformEngine.getTransformConfig()` method typically reads a `json` file.
|
||||
The names in the config should match the names returned by the `CustomTransformer`
|
||||
implementations.
|
||||
|
||||
~~~
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<body>
|
||||
<div>
|
||||
<h2>Test Transformation</h2>
|
||||
<form method="POST" enctype="multipart/form-data" action="/transform">
|
||||
<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">file *</div></td><td><input type="file" name="file" /></td></tr>
|
||||
<tr><td><div style="text-align:right">sourceExtension *</div></td><td><input type="text" name="sourceExtension" value="" /></td></tr>
|
||||
<tr><td><div style="text-align:right">targetExtension *</div></td><td><input type="text" name="targetExtension" value="" /></td></tr>
|
||||
<tr><td><div style="text-align:right">sourceMimetype *</div></td><td><input type="text" name="sourceMimetype" value="" /></td></tr>
|
||||
<tr><td><div style="text-align:right">targetMimetype *</div></td><td><input type="text" name="targetMimetype" value="" /></td></tr>
|
||||
<tr><td><div style="text-align:right">abc:width</div></td><td><input type="text" name="width" value="" /></td></tr>
|
||||
<tr><td><div style="text-align:right">abc:height</div></td><td><input type="text" name="height" value="" /></td></tr>
|
||||
<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>
|
||||
<a href="/log">Log</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
~~~
|
||||
|
||||
* *TransformerName*Controller.java - A [Spring Boot](https://projects.spring.io/spring-boot/) Controller that
|
||||
extends TransformController to handel requests. It implements a few methods including *transformImpl*
|
||||
which is intended to perform the actual transform. Generally the transform is done in a sub class of
|
||||
*JavaExecutor*, when a Java library is being used or *AbstractCommandExecutor*, when an external process is used.
|
||||
Both are sub interfaces of *Transformer*.
|
||||
**Example TransformEngine**
|
||||
|
||||
~~~
|
||||
...
|
||||
@Controller
|
||||
public class TransformerNameController extends TransformController
|
||||
The `TransformEngineName` is important if the config from multiple T-Engines is being
|
||||
combined as they are sorted by name.
|
||||
```
|
||||
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.common.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
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(TransformerNameController.class);
|
||||
@Autowired
|
||||
private TransformConfigResourceReader transformConfigResourceReader;
|
||||
|
||||
TransformerNameExecutor executor;
|
||||
|
||||
@PostConstruct
|
||||
private void init()
|
||||
@Override
|
||||
public String getTransformEngineName()
|
||||
{
|
||||
executor = new TransformerNameExecutor();
|
||||
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("jane.txt", "text/plain", "text/plain",
|
||||
ImmutableMap.of("sourceEncoding", "UTF-8", "language", "English"),
|
||||
11, 10, 150, 1024, 1, 60 * 2);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Example CustomTransformer**
|
||||
```
|
||||
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 "Transformer Name";
|
||||
return "hello";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String version()
|
||||
public void transform(String sourceMimetype, InputStream inputStream, String targetMimetype,
|
||||
OutputStream outputStream, Map<String, String> transformOptions, TransformManager transformManager)
|
||||
throws Exception
|
||||
{
|
||||
return commandExecutor.version();
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProbeTestTransform getProbeTestTransform()
|
||||
private String getGreeting(String language)
|
||||
{
|
||||
// See the Javadoc on this method and Probes.md for the choice of these values.
|
||||
return new ProbeTestTransform(this, "quick.pdf", "quick.png",
|
||||
7455, 1024, 150, 10240, 60 * 20 + 1, 60 * 15 - 15)
|
||||
{
|
||||
@Override
|
||||
protected void executeTransformCommand(File sourceFile, File targetFile)
|
||||
{
|
||||
transformImpl(null, null, null, Collections.emptyMap(), sourceFile, targetFile);
|
||||
}
|
||||
};
|
||||
return "Hello %s";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transformImpl(String transformName, String sourceMimetype, String targetMimetype,
|
||||
Map<String, String> transformOptions, File sourceFile, File targetFile)
|
||||
{
|
||||
executor.transform(sourceMimetype, targetMimetype, transformOptions, sourceFile, targetFile);
|
||||
}
|
||||
}
|
||||
~~~
|
||||
|
||||
* *TransformerName*Executer.java - *JavaExecutor* and *CommandExecutor* sub classes need to extract values from
|
||||
*transformOptions* and use them in a call to an external process or as parameters to a library call.
|
||||
~~~
|
||||
...
|
||||
public class TransformerNameExecutor extends AbstractCommandExecutor
|
||||
{
|
||||
...
|
||||
@Override
|
||||
public void transform(String transformName, String sourceMimetype, String targetMimetype,
|
||||
Map<String, String> transformOptions,
|
||||
File sourceFile, File targetFile) throws TransformException
|
||||
{
|
||||
final String options = TransformerNameOptionsBuilder
|
||||
.builder()
|
||||
.withWidth(transformOptions.get(WIDTH_REQUEST_PARAM))
|
||||
.withHeight(transformOptions.get(HEIGHT_REQUEST_PARAM))
|
||||
.build();
|
||||
|
||||
Long timeout = stringToLong(transformOptions.get(TIMEOUT));
|
||||
|
||||
run(options, sourceFile, targetFile, timeout);
|
||||
}
|
||||
}
|
||||
~~~
|
||||
|
||||
* Application.java - [Spring Boot](https://projects.spring.io/spring-boot/) expects to find an Application in
|
||||
a project's source files. The following may be used:
|
||||
|
||||
~~~
|
||||
package org.alfresco.transformer;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class Application
|
||||
{
|
||||
public static void main(String[] args)
|
||||
{
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
}
|
||||
~~~
|
||||
|
||||
Transform requests are handled by the *TransformController*, but are either:
|
||||
* POST requests (a direct http request from a client) where the transform options are passed as parameters, the source is supplied as a multipart file and
|
||||
the response is a file download.
|
||||
* POST request (a request via a message queue) where the transform options are supplied as JSON and the response is also JSON.
|
||||
The source and target content is read from a location accessible to both the client and the transfomer.
|
||||
|
||||
**Example JSON request body**
|
||||
```javascript
|
||||
var transformRequest = {
|
||||
"requestId": "1",
|
||||
"sourceReference": "2f9ed237-c734-4366-8c8b-6001819169a4",
|
||||
"sourceMediaType": "application/pdf",
|
||||
"sourceSize": 123456,
|
||||
"sourceExtension": "pdf",
|
||||
"targetMediaType": "text/plain",
|
||||
"targetExtension": "txt",
|
||||
"clientType": "ACS",
|
||||
"clientData": "Yo No Soy Marinero, Soy Capitan, Soy Capitan!",
|
||||
"schema": 1,
|
||||
"transformRequestOptions": {
|
||||
"targetMimetype": "text/plain",
|
||||
"targetEncoding": "UTF-8",
|
||||
"abc:width": "120",
|
||||
"abc:height": "200"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Example JSON response body**
|
||||
|
||||
```javascript
|
||||
var transformReply = {
|
||||
"requestId": "1",
|
||||
"status": 201,
|
||||
"errorDetails": null,
|
||||
"sourceReference": "2f9ed237-c734-4366-8c8b-6001819169a4",
|
||||
"targetReference": "34d69ff0-7eaa-4741-8a9f-e1915e6995bf",
|
||||
"clientType": "ACS",
|
||||
"clientData": "Yo No Soy Marinero, Soy Capitan, Soy Capitan!",
|
||||
"schema": 1
|
||||
**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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Building and testing
|
||||
|
||||
The project can be built by running the Maven command:
|
||||
|
||||
~~~
|
||||
mvn clean install
|
||||
~~~
|
||||
|
||||
## Artifacts
|
||||
|
||||
The artifacts can be obtained by:
|
||||
|
||||
* downloading from the [Alfresco repository](https://artifacts.alfresco.com/nexus/content/groups/public/)
|
||||
* Adding a Maven dependency to your pom file.
|
||||
|
||||
~~~
|
||||
<dependency>
|
||||
<groupId>org.alfresco</groupId>
|
||||
<artifactId>alfresco-base-t-engine</artifactId>
|
||||
<version>1.0</version>
|
||||
</dependency>
|
||||
~~~
|
||||
|
||||
and the Alfresco Maven repository:
|
||||
|
||||
~~~
|
||||
<repository>
|
||||
<id>alfresco-maven-repo</id>
|
||||
<url>https://artifacts.alfresco.com/nexus/content/groups/public</url>
|
||||
</repository>
|
||||
~~~
|
||||
|
||||
The build plan is available in [TravisCI](https://travis-ci.com/Alfresco/alfresco-transform-core).
|
||||
|
||||
## Contributing guide
|
||||
|
||||
Please use [this guide](https://github.com/Alfresco/alfresco-repository/blob/master/CONTRIBUTING.md)
|
||||
to make a contribution to the project.
|
||||
|
||||
**Example `ProbeTransform` test file** `jane.txt`
|
||||
```json
|
||||
Jane
|
||||
```
|
@@ -50,7 +50,9 @@ public interface TransformEngine
|
||||
|
||||
/**
|
||||
* @return a definition of what the t-engine supports. Normally read from a json Resource on the classpath using a
|
||||
* {@link TransformConfigResourceReader}.
|
||||
* {@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();
|
||||
|
||||
|
@@ -74,6 +74,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static org.alfresco.transform.base.fs.FileManager.createTargetFile;
|
||||
@@ -638,7 +639,10 @@ public class TransformHandler
|
||||
sourceSizeInBytes, targetMimetype, transformOptions, null);
|
||||
if (transformerName == null)
|
||||
{
|
||||
throw new TransformException(BAD_REQUEST, "No transforms were able to handle the request");
|
||||
throw new TransformException(BAD_REQUEST, "No transforms were able to handle the request: "+
|
||||
sourceMimetype+" -> "+targetMimetype+transformOptions.entrySet().stream()
|
||||
.map(entry -> entry.getKey()+"="+entry.getValue())
|
||||
.collect(Collectors.joining(", ", " with ", "")));
|
||||
}
|
||||
return transformerName;
|
||||
}
|
||||
|
@@ -51,9 +51,9 @@ 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
|
||||
* <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>
|
||||
*
|
||||
@@ -81,7 +81,6 @@ public class ProbeTransform
|
||||
|
||||
private static final int AVERAGE_OVER_TRANSFORMS = 5;
|
||||
private final String sourceFilename;
|
||||
private final String targetFilename;
|
||||
private final String sourceMimetype;
|
||||
private final String targetMimetype;
|
||||
private final Map<String, String> transformOptions;
|
||||
@@ -115,13 +114,11 @@ public class ProbeTransform
|
||||
return maxTime;
|
||||
}
|
||||
|
||||
public ProbeTransform(String sourceFilename, String targetFilename,
|
||||
String sourceMimetype, String targetMimetype, Map<String, String> transformOptions,
|
||||
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.targetFilename = targetFilename;
|
||||
this.sourceMimetype = sourceMimetype;
|
||||
this.targetMimetype = targetMimetype;
|
||||
this.transformOptions = new HashMap(transformOptions);
|
||||
@@ -271,14 +268,14 @@ public class ProbeTransform
|
||||
getMessagePrefix(isLiveProbe) + "Failed to store the source file", e);
|
||||
}
|
||||
long length = sourceFile.length();
|
||||
LogEntry.setSource(sourceFilename, length);
|
||||
LogEntry.setSource(sourceFile.getName(), length);
|
||||
return sourceFile;
|
||||
}
|
||||
|
||||
private File getTargetFile()
|
||||
{
|
||||
File targetFile = createTempFile("target_", "_" + targetFilename);
|
||||
LogEntry.setTarget(targetFilename);
|
||||
File targetFile = createTempFile("target_", "_" + sourceFilename);
|
||||
LogEntry.setTarget(targetFile.getName());
|
||||
return targetFile;
|
||||
}
|
||||
|
||||
|
@@ -1 +0,0 @@
|
||||
{}
|
@@ -46,11 +46,14 @@ import javax.jms.Destination;
|
||||
import javax.jms.JMSException;
|
||||
import javax.jms.Message;
|
||||
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doCallRealMethod;
|
||||
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.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
||||
import static org.springframework.http.HttpStatus.CREATED;
|
||||
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
@@ -187,13 +190,13 @@ public class QueueTransformServiceTest
|
||||
.build();
|
||||
|
||||
doReturn(request).when(transformMessageConverter).fromMessage(msg);
|
||||
doReturn(new ResponseEntity<>(reply, HttpStatus.valueOf(reply.getStatus())))
|
||||
.when(transformHandler).handleMessageRequest(request, null, null);
|
||||
doAnswer(invocation -> {transformReplySender.send(destination, reply); return null;})
|
||||
.when(transformHandler).handleMessageRequest(request, null, destination);
|
||||
|
||||
queueTransformService.receive(msg);
|
||||
|
||||
verify(transformMessageConverter).fromMessage(msg);
|
||||
verify(transformHandler).handleMessageRequest(request, null, null);
|
||||
verify(transformHandler).handleMessageRequest(request, null, destination);
|
||||
verify(transformReplySender).send(destination, reply);
|
||||
}
|
||||
|
||||
@@ -228,13 +231,13 @@ public class QueueTransformServiceTest
|
||||
.build();
|
||||
|
||||
doReturn(request).when(transformMessageConverter).fromMessage(msg);
|
||||
doReturn(new ResponseEntity<>(reply, HttpStatus.valueOf(reply.getStatus())))
|
||||
.when(transformHandler).handleMessageRequest(request, null, null);
|
||||
doAnswer(invocation -> {transformReplySender.send(destination, reply); return null;})
|
||||
.when(transformHandler).handleMessageRequest(request, null, destination);
|
||||
|
||||
queueTransformService.receive(msg);
|
||||
|
||||
verify(transformMessageConverter).fromMessage(msg);
|
||||
verify(transformHandler).handleMessageRequest(request, null, null);
|
||||
verify(transformHandler).handleMessageRequest(request, null, destination);
|
||||
verify(transformReplySender).send(destination, reply);
|
||||
}
|
||||
}
|
||||
|
@@ -46,7 +46,8 @@ import static org.alfresco.transform.common.RequestParamMap.SOURCE_ENCODING;
|
||||
|
||||
public class FakeTransformEngineWithTwoCustomTransformers extends AbstractFakeTransformEngine
|
||||
{
|
||||
@Override public TransformConfig getTransformConfig()
|
||||
@Override
|
||||
public TransformConfig getTransformConfig()
|
||||
{
|
||||
String docOptions = "docOptions";
|
||||
String imageOptions = "imageOptions";
|
||||
@@ -103,11 +104,11 @@ public class FakeTransformEngineWithTwoCustomTransformers extends AbstractFakeTr
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override public ProbeTransform getProbeTransform()
|
||||
@Override
|
||||
public ProbeTransform getProbeTransform()
|
||||
{
|
||||
return new ProbeTransform("quick.txt", "quick.pdf",
|
||||
MIMETYPE_TEXT_PLAIN, MIMETYPE_PDF, ImmutableMap.of(SOURCE_ENCODING, "UTF-8"),
|
||||
46, 0, 150, 1024, 1,
|
||||
60 * 2);
|
||||
return new ProbeTransform("quick.txt", MIMETYPE_TEXT_PLAIN, MIMETYPE_PDF,
|
||||
ImmutableMap.of(SOURCE_ENCODING, "UTF-8"), 46, 0,
|
||||
150, 1024, 1, 60 * 2);
|
||||
}
|
||||
}
|
||||
|
1
engines/example/.maven-dockerignore
Normal file
1
engines/example/.maven-dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
target/docker/
|
26
engines/example/Dockerfile
Normal file
26
engines/example/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
||||
FROM alfresco/alfresco-base-java:jre11-centos7-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
|
||||
|
||||
ENV JAVA_OPTS=""
|
||||
|
||||
ARG GROUPNAME=Alfresco
|
||||
ARG GROUPID=1000
|
||||
ARG EXAMPLEUSERNAME=example
|
||||
ARG USERID=33009
|
||||
|
||||
COPY target/${env.project_artifactId}-${env.project_version}.jar /usr/bin
|
||||
|
||||
RUN ln /usr/bin/${env.project_artifactId}-${env.project_version}.jar /usr/bin/${env.project_artifactId}.jar
|
||||
|
||||
RUN groupadd -g ${GROUPID} ${GROUPNAME} && \
|
||||
useradd -u ${USERID} -G ${GROUPNAME} ${EXAMPLEUSERNAME} && \
|
||||
chgrp -R ${GROUPNAME} /usr/bin/${env.project_artifactId}.jar
|
||||
|
||||
EXPOSE 8090
|
||||
|
||||
USER ${EXAMPLEUSERNAME}
|
||||
|
||||
ENTRYPOINT java $JAVA_OPTS -jar /usr/bin/${env.project_artifactId}.jar
|
300
engines/example/pom.xml
Normal file
300
engines/example/pom.xml
Normal file
@@ -0,0 +1,300 @@
|
||||
<?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-example</artifactId>
|
||||
<name>- Example</name>
|
||||
|
||||
<parent>
|
||||
<artifactId>alfresco-transform-core</artifactId>
|
||||
<groupId>org.alfresco</groupId>
|
||||
<version>2.6.1-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<image.name>alfresco/alfresco-transform-example</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.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<mainClass>org.alfresco.transform.base.Application</mainClass>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>license-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>test-jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</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.16.1</name>
|
||||
<run>
|
||||
<hostname>activemq</hostname>
|
||||
<ports>
|
||||
<port>8161:8161</port>
|
||||
<port>5672:5672</port>
|
||||
<port>61616:61616</port>
|
||||
</ports>
|
||||
<wait>
|
||||
<log>Apache ActiveMQ 5.16.1 .* 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>
|
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* #%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.example;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import org.alfresco.transform.base.TransformEngine;
|
||||
import org.alfresco.transform.base.probes.ProbeTransform;
|
||||
import org.alfresco.transform.common.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("jane.txt", "text/plain", "text/plain",
|
||||
ImmutableMap.of("sourceEncoding", "UTF-8", "language", "English"),
|
||||
11, 10, 150, 1024, 1, 60 * 2);
|
||||
}
|
||||
}
|
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* #%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.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";
|
||||
}
|
||||
}
|
19
engines/example/src/main/resources/hello_engine_config.json
Normal file
19
engines/example/src/main/resources/hello_engine_config.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"transformOptions": {
|
||||
"helloOptions": [
|
||||
{"value": {"name": "language"}},
|
||||
{"value": {"name": "sourceEncoding"}}
|
||||
]
|
||||
},
|
||||
"transformers": [
|
||||
{
|
||||
"transformerName": "hello",
|
||||
"supportedSourceAndTargetList": [
|
||||
{"sourceMediaType": "text/plain", "targetMediaType": "text/plain" }
|
||||
],
|
||||
"transformOptions": [
|
||||
"helloOptions"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
1
engines/example/src/main/resources/jane.txt
Normal file
1
engines/example/src/main/resources/jane.txt
Normal file
@@ -0,0 +1 @@
|
||||
Jane
|
@@ -68,8 +68,7 @@ public class ImageMagickTransformEngine implements TransformEngine
|
||||
@Override
|
||||
public ProbeTransform getProbeTransform()
|
||||
{
|
||||
return new ProbeTransform("quick.jpg", "quick.png",
|
||||
MIMETYPE_IMAGE_JPEG, MIMETYPE_IMAGE_PNG, Collections.emptyMap(),
|
||||
return new ProbeTransform("quick.jpg", MIMETYPE_IMAGE_JPEG, MIMETYPE_IMAGE_PNG, Collections.emptyMap(),
|
||||
35593, 1024, 150, 1024, 60 * 15 + 1, 60 * 15);
|
||||
}
|
||||
}
|
||||
|
@@ -69,8 +69,7 @@ public class LibreOfficeTransformEngine implements TransformEngine
|
||||
@Override
|
||||
public ProbeTransform getProbeTransform()
|
||||
{
|
||||
return new ProbeTransform("quick.doc", "quick.pdf",
|
||||
MIMETYPE_WORD, MIMETYPE_PDF, Collections.emptyMap(),
|
||||
return new ProbeTransform("quick.doc", MIMETYPE_WORD, MIMETYPE_PDF, Collections.emptyMap(),
|
||||
11817, 1024, 150, 10240, 60 * 30 + 1, 60 * 15 + 20);
|
||||
}
|
||||
}
|
||||
|
@@ -73,8 +73,7 @@ public class MiscTransformEngine implements TransformEngine
|
||||
@Override
|
||||
public ProbeTransform getProbeTransform()
|
||||
{
|
||||
return new ProbeTransform("quick.html", "quick.txt",
|
||||
MIMETYPE_HTML, MIMETYPE_TEXT_PLAIN, transformOptions,
|
||||
return new ProbeTransform("quick.html", MIMETYPE_HTML, MIMETYPE_TEXT_PLAIN, transformOptions,
|
||||
119, 30, 150, 1024, 60 * 2 + 1, 60 * 2);
|
||||
}
|
||||
}
|
||||
|
@@ -67,8 +67,7 @@ public class PdfRendererTransformEngine implements TransformEngine
|
||||
@Override
|
||||
public ProbeTransform getProbeTransform()
|
||||
{
|
||||
return new ProbeTransform("quick.pdf", "quick.png",
|
||||
MIMETYPE_PDF, MIMETYPE_IMAGE_PNG, Collections.emptyMap(),
|
||||
return new ProbeTransform("quick.pdf", MIMETYPE_PDF, MIMETYPE_IMAGE_PNG, Collections.emptyMap(),
|
||||
7455, 1024, 150, 10240, 60 * 20 + 1, 60 * 15 - 15);
|
||||
}
|
||||
}
|
@@ -68,8 +68,7 @@ public class TikaTransformEngine implements TransformEngine
|
||||
@Override
|
||||
public ProbeTransform getProbeTransform()
|
||||
{
|
||||
return new ProbeTransform("quick.pdf", "quick.txt",
|
||||
MIMETYPE_PDF, MIMETYPE_TEXT_PLAIN, Collections.emptyMap(),
|
||||
return new ProbeTransform("quick.pdf", MIMETYPE_PDF, MIMETYPE_TEXT_PLAIN, Collections.emptyMap(),
|
||||
60, 16, 400, 10240, 60 * 30 + 1, 60 * 15 + 20);
|
||||
}
|
||||
}
|
||||
|
@@ -87,9 +87,9 @@ class TransformRegistryHelper
|
||||
renditionName = null;
|
||||
}
|
||||
|
||||
final List<SupportedTransform> cachedTransformList =
|
||||
renditionName == null ? null :
|
||||
data.retrieveCached(renditionName, sourceMimetype);
|
||||
final List<SupportedTransform> cachedTransformList = renditionName == null
|
||||
? null
|
||||
: data.retrieveCached(renditionName, sourceMimetype);
|
||||
if (cachedTransformList != null)
|
||||
{
|
||||
return cachedTransformList;
|
||||
|
10
pom.xml
10
pom.xml
@@ -50,6 +50,7 @@
|
||||
<module>engines/pdfrenderer</module>
|
||||
<module>engines/tika</module>
|
||||
<module>engines/aio</module>
|
||||
<module>engines/example</module>
|
||||
<module>deprecated/alfresco-transformer-base</module>
|
||||
</modules>
|
||||
</profile>
|
||||
@@ -58,6 +59,7 @@
|
||||
<modules>
|
||||
<module>model</module>
|
||||
<module>engines/base</module>
|
||||
<module>engines/example</module>
|
||||
<module>deprecated/alfresco-transformer-base</module>
|
||||
</modules>
|
||||
</profile>
|
||||
@@ -110,6 +112,14 @@
|
||||
<module>engines/aio</module>
|
||||
</modules>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>example</id>
|
||||
<modules>
|
||||
<module>model</module>
|
||||
<module>engines/base</module>
|
||||
<module>engines/example</module>
|
||||
</modules>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<scm>
|
||||
|
Reference in New Issue
Block a user