Initial commit

This commit is contained in:
Alan Davis
2018-03-07 14:39:07 +00:00
commit 42e7e192b5
43 changed files with 3706 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2018 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.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);
}
}

View File

@@ -0,0 +1,330 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2018 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.transformer.base;
import org.alfresco.util.TempFileProvider;
import org.alfresco.util.exec.RuntimeExec;
import org.apache.commons.logging.Log;
import org.springframework.beans.TypeMismatchException;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
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.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.util.UriUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.Collection;
import java.util.Map;
/**
* Abstract Controller, provides structure and helper methods to sub-class transformer controllers.
*
* Status Codes:
*
* 200 Success
* 400 Bad Request: Request parameter <name> is missing (missing mandatory parameter)
* 400 Bad Request: Request parameter <name> is of the wrong type
* 400 Bad Request: Transformer exit code was not 0 (possible problem with the source file)
* 400 Bad Request: The source filename was not supplied
* 500 Internal Server Error: (no message with low level IO problems)
* 500 Internal Server Error: The target filename was not supplied (should not happen as targetExtension is checked)
* 500 Internal Server Error: Transformer version check exit code was not 0
* 500 Internal Server Error: Transformer version check failed to create any output
* 500 Internal Server Error: Could not read the target file
* 500 Internal Server Error: The target filename was malformed (should not happen because of other checks)
* 500 Internal Server Error: Transformer failed to create an output file (the exit code was 0, so there should be some content)
* 500 Internal Server Error: Filename encoding error
* 507 Insufficient Storage: Failed to store the source file
*
* 408 Request Timeout -- TODO implement general timeout mechanism rather than depend on transformer timeout (might be possible for external processes)
* 415 Unsupported Media Type -- TODO possibly implement a check on supported source and target mimetypes (probably not)
* 429 Too Many Requests -- TODO implement general throttling mechanism (needs to be done)
*/
public abstract class AbstractTransformerController
{
public static final String SOURCE_FILE = "sourceFile";
public static final String TARGET_FILE = "targetFile";
protected static Log logger;
protected RuntimeExec transformCommand;
private RuntimeExec checkCommand;
public void setTransformCommand(RuntimeExec runtimeExec)
{
transformCommand = runtimeExec;
}
public void setCheckCommand(RuntimeExec runtimeExec)
{
checkCommand = runtimeExec;
}
@RequestMapping("/version")
@ResponseBody
String version()
{
String version = "Version not checked";
if (checkCommand != null)
{
RuntimeExec.ExecutionResult result = checkCommand.execute();
if (result.getExitValue() != 0 && result.getStdErr() != null && result.getStdErr().length() > 0)
{
throw new TransformException(500, "Transformer version check exit code was not 0: \n" + result);
}
version = result.getStdOut().trim();
if (version.isEmpty())
{
throw new TransformException(500, "Transformer version check failed to create any output");
}
}
return version;
}
@GetMapping("/")
public String transformForm(Model model)
{
return "transformForm"; // the name of the template
}
@GetMapping("/log")
public String log(Model model)
{
Collection<LogEntry> log = LogEntry.getLog();
if (!log.isEmpty())
{
model.addAttribute("log", log);
}
return "log"; // the name of the template
}
@ExceptionHandler(TypeMismatchException.class)
public void handleParamsTypeMismatch(HttpServletResponse response, MissingServletRequestParameterException e) throws IOException
{
String name = e.getParameterName();
String message = "Request parameter " + name + " is of the wrong type";
int statusCode = 400;
if (logger != null && logger.isErrorEnabled())
{
logger.error(message);
}
LogEntry.setStatusCodeAndMessage(statusCode, message);
response.sendError(statusCode, message);
}
@ExceptionHandler(MissingServletRequestParameterException.class)
public void handleMissingParams(HttpServletResponse response, MissingServletRequestParameterException e) throws IOException
{
String name = e.getParameterName();
String message = "Request parameter " + name + " is missing";
int statusCode = 400;
if (logger != null && logger.isErrorEnabled())
{
logger.error(message);
}
LogEntry.setStatusCodeAndMessage(statusCode, message);
response.sendError(statusCode, message);
}
@ExceptionHandler(TransformException.class)
public void transformExceptionWithMessage(HttpServletResponse response, TransformException e) throws IOException
{
String message = e.getMessage();
int statusCode = e.getStatusCode();
if (logger != null && logger.isErrorEnabled())
{
logger.error(message);
}
LogEntry.setStatusCodeAndMessage(statusCode, message);
response.sendError(statusCode, message);
}
protected String createTargetFileName(MultipartFile sourceMultipartFile, String targetExtension)
{
String targetFilename = null;
String sourceFilename = sourceMultipartFile.getOriginalFilename();
sourceFilename = StringUtils.getFilename(sourceFilename);
if (sourceFilename != null && !sourceFilename.isEmpty())
{
String ext = StringUtils.getFilenameExtension(sourceFilename);
if (ext != null && !ext.isEmpty())
{
targetFilename =sourceFilename.substring(0, sourceFilename.length()-ext.length()-1)+'.'+targetExtension;
}
}
return targetFilename;
}
/**
* Returns a File that holds the source content for a transformation.
*
* @param request
* @param multipartFile from the request
* @return a temporary File.
* @throws TransformException if there was no source filename.
*/
protected File createSourceFile(HttpServletRequest request, MultipartFile multipartFile)
{
String filename = multipartFile.getOriginalFilename();
long size = multipartFile.getSize();
filename = checkFilename( true, filename);
File file = TempFileProvider.createTempFile("source_", "_" + filename);
request.setAttribute(SOURCE_FILE, file);
save(multipartFile, file);
LogEntry.setSource(filename, size);
return file;
}
/**
* Returns a File to be used to store the result of a transformation.
*
* @param request
* @param filename The targetFilename supplied in the request. Only the filename if a path is used as part of the
* temporary filename.
* @return a temporary File.
* @throws TransformException if there was no target filename.
*/
protected File createTargetFile(HttpServletRequest request, String filename)
{
filename = checkFilename( false, filename);
LogEntry.setTarget(filename);
File file = TempFileProvider.createTempFile("target_", "_" + filename);
request.setAttribute(TARGET_FILE, file);
return file;
}
/**
* Checks the filename is okay to uses in a temporary file name.
*
* @param filename or path to be checked.
* @return the filename part of the supplied filename if it was a path.
* @throws TransformException if there was no target filename.
*/
private String checkFilename(boolean source, String filename)
{
filename = StringUtils.getFilename(filename);
if (filename == null || filename.isEmpty())
{
String sourceOrTarget = source ? "source" : "target";
int statusCode = source ? 400 : 500;
throw new TransformException(statusCode, "The " + sourceOrTarget + " filename was not supplied");
}
return filename;
}
private void save(MultipartFile multipartFile, File file)
{
try
{
Files.copy(multipartFile.getInputStream(), file.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
catch (IOException e)
{
throw new TransformException(507, "Failed to store the source file", e);
}
}
private Resource load(File file)
{
try
{
Resource resource = new UrlResource(file.toURI());
if (resource.exists() || resource.isReadable())
{
return resource;
}
else
{
throw new TransformException(500, "Could not read the target file: " + file.getPath());
}
}
catch (MalformedURLException e)
{
throw new TransformException(500, "The target filename was malformed: " + file.getPath(), e);
}
}
protected void executeTransformCommand(Map<String, String> properties, File targetFile, Long timeout)
{
long timeoutMs = timeout != null && timeout > 0 ? timeout : 0;
RuntimeExec.ExecutionResult result = transformCommand.execute(properties, timeoutMs);
if (result.getExitValue() != 0 && result.getStdErr() != null && result.getStdErr().length() > 0)
{
throw new TransformException(400, "Transformer exit code was not 0: \n" + result);
}
if (!targetFile.exists() || targetFile.length() == 0)
{
throw new TransformException(500, "Transformer failed to create an output file");
}
}
protected ResponseEntity<Resource> createAttachment(String targetFilename, File targetFile)
{
try
{
Resource targetResource = load(targetFile);
targetFilename = UriUtils.encodePath(StringUtils.getFilename(targetFilename), "UTF-8");
ResponseEntity<Resource> body = ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename*= UTF-8''" + targetFilename).body(targetResource);
LogEntry.setTargetSize(targetFile.length());
LogEntry.setStatusCodeAndMessage(200, "Success");
return body;
}
catch (UnsupportedEncodingException e)
{
throw new TransformException(500, "Filename encoding error", e);
}
}
}

View File

@@ -0,0 +1,248 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2018 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.transformer.base;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 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 class LogEntry
{
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 ThreadLocal<LogEntry> currentLogEntry = new ThreadLocal<LogEntry>()
{
@Override
protected LogEntry initialValue()
{
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;
private long durationStreamOut;
private String source;
private long sourceSize;
private String target;
private long targetSize;
private String options;
private String message;
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(int statusCode, String message)
{
LogEntry logEntry = currentLogEntry.get();
logEntry.statusCode = statusCode;
logEntry.message = message;
logEntry.durationTransform = System.currentTimeMillis() - logEntry.start - logEntry.durationStreamIn;
}
public static void complete()
{
LogEntry logEntry = currentLogEntry.get();
logEntry.durationStreamOut = System.currentTimeMillis() - logEntry.start - logEntry.durationStreamIn - logEntry.durationTransform;
currentLogEntry.remove();
}
public int getId()
{
return id;
}
public Date getDate()
{
return new Date(start);
}
public int getStatusCode()
{
return statusCode;
}
public String getDuration()
{
return time(durationStreamIn + durationTransform + durationStreamOut)+" ("+
time(durationStreamIn)+' '+time(durationTransform)+' '+time(durationStreamOut)+")";
}
public long getDurationStreamIn()
{
return durationStreamIn;
}
public long getDurationTransform()
{
return durationTransform;
}
public long getDurationStreamOut()
{
return durationStreamOut;
}
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 size(ms, "1 ms",
new String[] { "ms", "s", "min", "hr" },
new long[] { 1000, 60*1000, 60*60*1000, Long.MAX_VALUE});
}
private String size(long size)
{
return size(size, "1 byte",
new String[] { "bytes", "KB", "MB", "GB", "TB" },
new long[] { 1024, 1024*1024, 1024*1024*1024, 1024*1024*1024*1024, Long.MAX_VALUE });
}
private String size(long size, String singleValue, String[] units, long[] dividers)
{
if (size == 1)
{
return singleValue;
}
long divider = 1;
for(int i = 0; i < units.length-1; i++)
{
long nextDivider = dividers[i];
if(size < nextDivider)
{
return unitFormat(size, divider, units[i]);
}
divider = nextDivider;
}
return unitFormat(size, divider, units[units.length-1]);
}
private String unitFormat(long size, long divider, String unit)
{
size = size * 10 / divider;
int decimalPoint = (int) size % 10;
StringBuilder sb = new StringBuilder();
sb.append(size/10);
if (decimalPoint != 0)
{
sb.append(".");
sb.append(decimalPoint);
}
sb.append(' ');
sb.append(unit);
return sb.toString();
}
}

View File

@@ -0,0 +1,47 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2018 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.transformer.base;
public class TransformException extends RuntimeException
{
private int statusCode;
public TransformException(int statusCode, String message)
{
super(message);
this.statusCode = statusCode;
}
public TransformException(int statusCode, String message, Throwable cause)
{
super(message, cause);
}
public int getStatusCode()
{
return statusCode;
}
}

View File

@@ -0,0 +1,65 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2018 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.transformer.base;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
public class TransformInterceptor extends HandlerInterceptorAdapter
{
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception
{
LogEntry.start();
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)
throws Exception
{
// TargetFile cannot be deleted until completion, otherwise 0 bytes are sent.
deleteFile(request, "sourceFile");
deleteFile(request, "targetFile");
LogEntry.complete();
}
private void deleteFile(HttpServletRequest request, String attributeName)
{
File file = (File) request.getAttribute(attributeName);
if (file != null)
{
file.delete();
}
}
}

View File

@@ -0,0 +1,45 @@
/*
* #%L
* Alfresco Repository
* %%
* Copyright (C) 2005 - 2018 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.transformer.base;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class WebApplicationConfig extends WebMvcConfigurerAdapter {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(transformInterceptor()).addPathPatterns("/transform");;
}
@Bean
public TransformInterceptor transformInterceptor() {
return new TransformInterceptor();
}
}

View File

@@ -0,0 +1,7 @@
spring.http.multipart.max-file-size=8192MB
spring.http.multipart.max-request-size=8192MB
server.port = 8090
logging.level.org.alfresco.util.exec.RuntimeExec=debug
logging.level.org.alfresco.transformer.LibreOfficeController=debug
logging.level.org.alfresco.transformer.JodConverterSharedInstance=debug

View File

@@ -0,0 +1,45 @@
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div th:if="${message}">
<h2 th:text="${message}"/>
</div>
<h2>Log entries</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>
<br/>
<a href="/">Test Transformation</a>
</div>
</body>
</html>