diff --git a/alfresco-docker-alfresco-pdf-renderer/src/main/java/org/alfresco/transformer/AlfrescoPdfRendererController.java b/alfresco-docker-alfresco-pdf-renderer/src/main/java/org/alfresco/transformer/AlfrescoPdfRendererController.java index ab7a230b..0e96c360 100644 --- a/alfresco-docker-alfresco-pdf-renderer/src/main/java/org/alfresco/transformer/AlfrescoPdfRendererController.java +++ b/alfresco-docker-alfresco-pdf-renderer/src/main/java/org/alfresco/transformer/AlfrescoPdfRendererController.java @@ -11,14 +11,23 @@ */ package org.alfresco.transformer; +import static org.alfresco.transformer.fs.FileManager.createAttachment; +import static org.alfresco.transformer.fs.FileManager.createSourceFile; +import static org.alfresco.transformer.fs.FileManager.createTargetFile; +import static org.alfresco.transformer.fs.FileManager.createTargetFileName; +import static org.alfresco.transformer.logging.StandardMessages.ENTERPRISE_LICENCE; + import java.io.File; -import java.util.HashMap; +import java.util.Arrays; import java.util.Map; import java.util.StringJoiner; import javax.servlet.http.HttpServletRequest; -import org.alfresco.util.exec.RuntimeExec; +import org.alfresco.transformer.executors.PdfRendererCommandExecutor; +import org.alfresco.transformer.logging.LogEntry; +import org.alfresco.transformer.probes.ProbeTestTransform; +import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; @@ -52,69 +61,49 @@ import org.springframework.web.multipart.MultipartFile; @Controller public class AlfrescoPdfRendererController extends AbstractTransformerController { - private static final String EXE = "/usr/bin/alfresco-pdf-renderer"; + private static final Log logger = LogFactory.getLog(AlfrescoPdfRendererController.class); + + @Autowired + private PdfRendererCommandExecutor commandExecutor; @Autowired public AlfrescoPdfRendererController() { - logger = LogFactory.getLog(AlfrescoPdfRendererController.class); logger.info("-----------------------------------------------------------------------------------------------------------------------------------------------------------"); - logEnterpriseLicenseMessage(); + Arrays.stream(ENTERPRISE_LICENCE.split("\\n")).forEach(logger::info); logger.info("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"); logger.info("-----------------------------------------------------------------------------------------------------------------------------------------------------------"); - setTransformCommand(createTransformCommand()); - setCheckCommand(createCheckCommand()); } @Override - protected String getTransformerName() + public String getTransformerName() { return "Alfresco PDF Renderer"; } - private static RuntimeExec createTransformCommand() + @Override + public String version() { - RuntimeExec runtimeExec = new RuntimeExec(); - Map commandsAndArguments = new HashMap<>(); - commandsAndArguments.put(".*", new String[]{EXE, "SPLIT:${options}", "${source}", "${target}"}); - runtimeExec.setCommandsAndArguments(commandsAndArguments); - - Map defaultProperties = new HashMap<>(); - defaultProperties.put("key", null); - runtimeExec.setDefaultProperties(defaultProperties); - - runtimeExec.setErrorCodes("1"); - - return runtimeExec; - } - - private static RuntimeExec createCheckCommand() - { - RuntimeExec runtimeExec = new RuntimeExec(); - Map commandsAndArguments = new HashMap<>(); - commandsAndArguments.put(".*", new String[]{EXE, "--version"}); - runtimeExec.setCommandsAndArguments(commandsAndArguments); - - return runtimeExec; + return commandExecutor.version(); } @Override - protected ProbeTestTransform getProbeTestTransform() + public ProbeTestTransform getProbeTestTransform() { // See the Javadoc on this method and Probes.md for the choice of these values. - return new ProbeTestTransform(this,"quick.pdf", "quick.png", + return new ProbeTestTransform(this, logger, "quick.pdf", "quick.png", 7455, 1024, 150, 10240, 60*20+1, 60*15-15) { @Override protected void executeTransformCommand(File sourceFile, File targetFile) { - AlfrescoPdfRendererController.this.executeTransformCommand("", sourceFile, targetFile, null); + commandExecutor.run("", sourceFile, targetFile, null); } }; } @Override - protected void processTransform(File sourceFile, File targetFile, + public void processTransform(File sourceFile, File targetFile, Map transformOptions, Long timeout) { String page = transformOptions.get("page"); @@ -137,7 +126,7 @@ public class AlfrescoPdfRendererController extends AbstractTransformerController String options = buildTransformOptions(pageOption, widthOption, heightOption, allowEnlargementOption, maintainAspectRatioOption); - executeTransformCommand(options, sourceFile, targetFile, timeout); + commandExecutor.run(options, sourceFile, targetFile, timeout); } @Deprecated @@ -155,19 +144,26 @@ public class AlfrescoPdfRendererController extends AbstractTransformerController @RequestParam(value = "maintainAspectRatio", required = false) Boolean maintainAspectRatio) { String targetFilename = createTargetFileName(sourceMultipartFile.getOriginalFilename(), targetExtension); + getProbeTestTransform().incrementTransformerCount(); File sourceFile = createSourceFile(request, sourceMultipartFile); File targetFile = createTargetFile(request, targetFilename); // Both files are deleted by TransformInterceptor.afterCompletion String options = buildTransformOptions(page, width, height, allowEnlargement, maintainAspectRatio); - executeTransformCommand(options, sourceFile, targetFile, timeout); - - return createAttachment(targetFilename, targetFile, testDelay); + commandExecutor.run(options, sourceFile, targetFile, timeout); + + final ResponseEntity body = createAttachment(targetFilename, targetFile); + LogEntry.setTargetSize(targetFile.length()); + long time = LogEntry.setStatusCodeAndMessage(200, "Success"); + time += LogEntry.addDelay(testDelay); + getProbeTestTransform().recordTransformTime(time); + return body; } - public String buildTransformOptions(Integer page,Integer width,Integer height,Boolean allowEnlargement,Boolean maintainAspectRatio) + private static String buildTransformOptions(Integer page,Integer width,Integer height,Boolean + allowEnlargement,Boolean maintainAspectRatio) { StringJoiner args = new StringJoiner(" "); if (width != null && width >= 0) diff --git a/alfresco-docker-alfresco-pdf-renderer/src/main/java/org/alfresco/transformer/Application.java b/alfresco-docker-alfresco-pdf-renderer/src/main/java/org/alfresco/transformer/Application.java index ec500713..20629a64 100644 --- a/alfresco-docker-alfresco-pdf-renderer/src/main/java/org/alfresco/transformer/Application.java +++ b/alfresco-docker-alfresco-pdf-renderer/src/main/java/org/alfresco/transformer/Application.java @@ -12,6 +12,8 @@ package org.alfresco.transformer; import io.micrometer.core.instrument.MeterRegistry; + +import org.alfresco.transformer.executors.PdfRendererCommandExecutor; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; @@ -27,9 +29,15 @@ public class Application @Value("${container.name}") private String containerName; - @Bean MeterRegistryCustomizer metricsCommonTags() { + @Bean + public MeterRegistryCustomizer metricsCommonTags() { return registry -> registry.config().commonTags("containerName", containerName); } + + @Bean + public PdfRendererCommandExecutor commandExecutor() { + return new PdfRendererCommandExecutor(); + } public static void main(String[] args) { diff --git a/alfresco-docker-alfresco-pdf-renderer/src/main/java/org/alfresco/transformer/executors/PdfRendererCommandExecutor.java b/alfresco-docker-alfresco-pdf-renderer/src/main/java/org/alfresco/transformer/executors/PdfRendererCommandExecutor.java new file mode 100644 index 00000000..c735ffea --- /dev/null +++ b/alfresco-docker-alfresco-pdf-renderer/src/main/java/org/alfresco/transformer/executors/PdfRendererCommandExecutor.java @@ -0,0 +1,43 @@ +package org.alfresco.transformer.executors; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.util.exec.RuntimeExec; +import org.springframework.stereotype.Component; + +/** + */ +@Component +public class PdfRendererCommandExecutor extends AbstractCommandExecutor +{ + private static final String EXE = "/usr/bin/alfresco-pdf-renderer"; + + @Override + protected RuntimeExec createTransformCommand() + { + RuntimeExec runtimeExec = new RuntimeExec(); + Map commandsAndArguments = new HashMap<>(); + commandsAndArguments.put(".*", + new String[]{EXE, "SPLIT:${options}", "${source}", "${target}"}); + runtimeExec.setCommandsAndArguments(commandsAndArguments); + + Map defaultProperties = new HashMap<>(); + defaultProperties.put("key", null); + runtimeExec.setDefaultProperties(defaultProperties); + + runtimeExec.setErrorCodes("1"); + + return runtimeExec; + } + + @Override + protected RuntimeExec createCheckCommand() + { + RuntimeExec runtimeExec = new RuntimeExec(); + Map commandsAndArguments = new HashMap<>(); + commandsAndArguments.put(".*", new String[]{EXE, "--version"}); + runtimeExec.setCommandsAndArguments(commandsAndArguments); + return runtimeExec; + } +} diff --git a/alfresco-docker-alfresco-pdf-renderer/src/test/java/org/alfresco/transformer/AlfrescoPdfRendererControllerTest.java b/alfresco-docker-alfresco-pdf-renderer/src/test/java/org/alfresco/transformer/AlfrescoPdfRendererControllerTest.java index 9340cd3b..25e58846 100644 --- a/alfresco-docker-alfresco-pdf-renderer/src/test/java/org/alfresco/transformer/AlfrescoPdfRendererControllerTest.java +++ b/alfresco-docker-alfresco-pdf-renderer/src/test/java/org/alfresco/transformer/AlfrescoPdfRendererControllerTest.java @@ -25,19 +25,48 @@ */ package org.alfresco.transformer; +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; 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; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import org.alfresco.transform.client.model.TransformReply; import org.alfresco.transform.client.model.TransformRequest; +import org.alfresco.transformer.executors.PdfRendererCommandExecutor; +import org.alfresco.transformer.model.FileRefEntity; +import org.alfresco.transformer.model.FileRefResponse; +import org.alfresco.util.exec.RuntimeExec; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.stubbing.Answer; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.util.StringUtils; /** * Test the AlfrescoPdfRendererController without a server. @@ -47,22 +76,111 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @WebMvcTest(AlfrescoPdfRendererController.class) public class AlfrescoPdfRendererControllerTest extends AbstractTransformerControllerTest { + @Mock + private RuntimeExec.ExecutionResult mockExecutionResult; + + @Mock + private RuntimeExec mockTransformCommand; + + @Mock + private RuntimeExec mockCheckCommand; + + @SpyBean + private PdfRendererCommandExecutor commandExecutor; + @SpyBean private AlfrescoPdfRendererController controller; @Before - public void before() throws Exception + public void before() throws IOException { - controller.setAlfrescoSharedFileStoreClient(alfrescoSharedFileStoreClient); - super.controller = controller; - super.mockTransformCommand(controller, "pdf", "png", "application/pdf", true); + commandExecutor.setTransformCommand(mockTransformCommand); + commandExecutor.setCheckCommand(mockCheckCommand); + + mockTransformCommand("pdf", "png", "application/pdf", true); + } + + @Override + public void mockTransformCommand(String sourceExtension, + String targetExtension, String sourceMimetype, + boolean readTargetFileBytes) throws IOException + { + this.sourceExtension = sourceExtension; + this.targetExtension = targetExtension; + this.sourceMimetype = sourceMimetype; + + expectedOptions = null; + expectedSourceSuffix = null; + expectedSourceFileBytes = readTestFile(sourceExtension); + expectedTargetFileBytes = readTargetFileBytes ? readTestFile(targetExtension) : null; + sourceFile = new MockMultipartFile("file", "quick." + sourceExtension, sourceMimetype, expectedSourceFileBytes); + + when(mockTransformCommand.execute(any(), anyLong())).thenAnswer( + (Answer) invocation -> { + Map actualProperties = invocation.getArgument(0); + assertEquals("There should be 3 properties", 3, actualProperties.size()); + + String actualOptions = actualProperties.get("options"); + String actualSource = actualProperties.get("source"); + String actualTarget = actualProperties.get("target"); + String actualTargetExtension = StringUtils.getFilenameExtension(actualTarget); + + assertNotNull(actualSource); + assertNotNull(actualTarget); + if (expectedSourceSuffix != null) + { + assertTrue("The source file \""+actualSource+"\" should have ended in \""+expectedSourceSuffix+"\"", actualSource.endsWith(expectedSourceSuffix)); + actualSource = actualSource.substring(0, actualSource.length()-expectedSourceSuffix.length()); + } + + assertNotNull(actualOptions); + if (expectedOptions != null) + { + assertEquals("expectedOptions", expectedOptions, actualOptions); + } + + Long actualTimeout = invocation.getArgument(1); + assertNotNull(actualTimeout); + if (expectedTimeout != null) + { + assertEquals("expectedTimeout", expectedTimeout, actualTimeout); + } + + // Copy a test file into the target file location if it exists + int i = actualTarget.lastIndexOf('_'); + if (i >= 0) + { + String testFilename = actualTarget.substring(i+1); + File testFile = getTestFile(testFilename, false); + File targetFile = new File(actualTarget); + generateTargetFileFromResourceFile(actualTargetExtension, testFile, + targetFile); + } + + // Check the supplied source file has not been changed. + byte[] actualSourceFileBytes = Files.readAllBytes(new File(actualSource).toPath()); + assertTrue("Source file is not the same", Arrays.equals(expectedSourceFileBytes, actualSourceFileBytes)); + + return mockExecutionResult; + }); + + when(mockExecutionResult.getExitValue()).thenReturn(0); + when(mockExecutionResult.getStdErr()).thenReturn("STDERROR"); + when(mockExecutionResult.getStdOut()).thenReturn("STDOUT"); + } + + + @Override + protected AbstractTransformerController getController() + { + return controller; } @Test public void optionsTest() throws Exception { expectedOptions = "--width=321 --height=654 --allow-enlargement --maintain-aspect-ratio --page=2"; - mockMvc.perform(MockMvcRequestBuilders.fileUpload("/transform") + mockMvc.perform(MockMvcRequestBuilders.multipart("/transform") .file(sourceFile) .param("targetExtension", targetExtension) @@ -82,7 +200,7 @@ public class AlfrescoPdfRendererControllerTest extends AbstractTransformerContro public void optionsNegateBooleansTest() throws Exception { expectedOptions = "--width=321 --height=654 --page=2"; - mockMvc.perform(MockMvcRequestBuilders.fileUpload("/transform") + mockMvc.perform(MockMvcRequestBuilders.multipart("/transform") .file(sourceFile) .param("targetExtension", targetExtension) @@ -106,4 +224,64 @@ public class AlfrescoPdfRendererControllerTest extends AbstractTransformerContro transformRequest.setSourceMediaType(MediaType.APPLICATION_PDF_VALUE); transformRequest.setTargetMediaType(MediaType.IMAGE_PNG_VALUE); } + + @Test + public void badExitCodeTest() throws Exception + { + when(mockExecutionResult.getExitValue()).thenReturn(1); + + mockMvc.perform(mockMvcRequest("/transform", sourceFile, "targetExtension", "xxx")) + .andExpect(status().is(400)) + .andExpect(status().reason(containsString("Transformer exit code was not 0: \nSTDERR"))); + } + + @Test + public void testPojoTransform() throws Exception + { + // Files + String sourceFileRef = UUID.randomUUID().toString(); + File sourceFile = getTestFile("quick." + sourceExtension, true); + String targetFileRef = UUID.randomUUID().toString(); + + + // Transformation Request POJO + TransformRequest transformRequest = new TransformRequest(); + transformRequest.setRequestId("1"); + transformRequest.setSchema(1); + transformRequest.setClientData("Alfresco Digital Business Platform"); + transformRequest.setTransformRequestOptions(new HashMap<>()); + transformRequest.setSourceReference(sourceFileRef); + transformRequest.setSourceExtension(sourceExtension); + transformRequest.setSourceSize(sourceFile.length()); + transformRequest.setTargetExtension(targetExtension); + + // HTTP Request + HttpHeaders headers = new HttpHeaders(); + headers.set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=quick." + sourceExtension); + ResponseEntity response = new ResponseEntity<>(new FileSystemResource( + sourceFile), headers, HttpStatus.OK); + + when(alfrescoSharedFileStoreClient.retrieveFile(sourceFileRef)).thenReturn(response); + when(alfrescoSharedFileStoreClient.saveFile(any())).thenReturn(new FileRefResponse(new FileRefEntity(targetFileRef))); + when(mockExecutionResult.getExitValue()).thenReturn(0); + + // 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("/transform") + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).content(tr)) + .andExpect(status().is(HttpStatus.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()); + } + } diff --git a/alfresco-docker-alfresco-pdf-renderer/src/test/java/org/alfresco/transformer/AlfrescoPdfRendererHttpRequestTest.java b/alfresco-docker-alfresco-pdf-renderer/src/test/java/org/alfresco/transformer/AlfrescoPdfRendererHttpRequestTest.java index 1d8d6e0e..cc9950b6 100644 --- a/alfresco-docker-alfresco-pdf-renderer/src/test/java/org/alfresco/transformer/AlfrescoPdfRendererHttpRequestTest.java +++ b/alfresco-docker-alfresco-pdf-renderer/src/test/java/org/alfresco/transformer/AlfrescoPdfRendererHttpRequestTest.java @@ -25,7 +25,6 @@ */ package org.alfresco.transformer; -import org.alfresco.transformer.AbstractHttpRequestTest; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; @@ -48,5 +47,5 @@ public class AlfrescoPdfRendererHttpRequestTest extends AbstractHttpRequestTest protected String getSourceExtension() { return "pdf"; - }; + } } \ No newline at end of file diff --git a/alfresco-docker-imagemagick/src/main/java/org/alfresco/transformer/Application.java b/alfresco-docker-imagemagick/src/main/java/org/alfresco/transformer/Application.java index 778b51db..aafdfeb8 100644 --- a/alfresco-docker-imagemagick/src/main/java/org/alfresco/transformer/Application.java +++ b/alfresco-docker-imagemagick/src/main/java/org/alfresco/transformer/Application.java @@ -12,6 +12,8 @@ package org.alfresco.transformer; import io.micrometer.core.instrument.MeterRegistry; + +import org.alfresco.transformer.executors.ImageMagickCommandExecutor; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; @@ -27,9 +29,15 @@ public class Application @Value("${container.name}") private String containerName; - @Bean MeterRegistryCustomizer metricsCommonTags() { + @Bean + MeterRegistryCustomizer metricsCommonTags() { return registry -> registry.config().commonTags("containerName", containerName); } + + @Bean + public ImageMagickCommandExecutor commandExecutor() { + return new ImageMagickCommandExecutor(); + } public static void main(String[] args) { diff --git a/alfresco-docker-imagemagick/src/main/java/org/alfresco/transformer/ImageMagickController.java b/alfresco-docker-imagemagick/src/main/java/org/alfresco/transformer/ImageMagickController.java index 87aee38f..883ec1f7 100644 --- a/alfresco-docker-imagemagick/src/main/java/org/alfresco/transformer/ImageMagickController.java +++ b/alfresco-docker-imagemagick/src/main/java/org/alfresco/transformer/ImageMagickController.java @@ -11,16 +11,27 @@ */ package org.alfresco.transformer; +import static org.alfresco.transformer.fs.FileManager.createAttachment; +import static org.alfresco.transformer.fs.FileManager.createSourceFile; +import static org.alfresco.transformer.fs.FileManager.createTargetFile; +import static org.alfresco.transformer.fs.FileManager.createTargetFileName; +import static org.alfresco.transformer.logging.StandardMessages.ENTERPRISE_LICENCE; +import static org.alfresco.transformer.util.Util.stringToBoolean; +import static org.alfresco.transformer.util.Util.stringToInteger; + import java.io.File; import java.util.Arrays; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.StringJoiner; import javax.servlet.http.HttpServletRequest; -import org.alfresco.util.exec.RuntimeExec; +import org.alfresco.transformer.exceptions.TransformException; +import org.alfresco.transformer.executors.ImageMagickCommandExecutor; +import org.alfresco.transformer.logging.LogEntry; +import org.alfresco.transformer.probes.ProbeTestTransform; +import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; @@ -56,73 +67,46 @@ import org.springframework.web.multipart.MultipartFile; @Controller public class ImageMagickController extends AbstractTransformerController { - private static final String ROOT = "/usr/lib64/ImageMagick-7.0.7"; - private static final String DYN = ROOT+"/lib"; - private static final String EXE = "/usr/bin/convert"; + private static final Log logger = LogFactory.getLog(ImageMagickController.class); + private static final List GRAVITY_VALUES = Arrays.asList( "North", "NorthEast", "East", "SouthEast", "South", "SouthWest", "West", "NorthWest", "Center"); + @Autowired + private ImageMagickCommandExecutor commandExecutor; + @Autowired public ImageMagickController() { - logger = LogFactory.getLog(ImageMagickController.class); logger.info("--------------------------------------------------------------------------------------------------------------------------------------------------------------"); - logEnterpriseLicenseMessage(); + Arrays.stream(ENTERPRISE_LICENCE.split("\\n")).forEach(logger::info); logger.info("This transformer uses ImageMagick from ImageMagick Studio LLC. See the license at http://www.imagemagick.org/script/license.php or in /ImageMagick-license.txt"); logger.info("--------------------------------------------------------------------------------------------------------------------------------------------------------------"); - setTransformCommand(createTransformCommand()); - setCheckCommand(createCheckCommand()); } @Override - protected String getTransformerName() + public String getTransformerName() { return "ImageMagick"; } - private static RuntimeExec createTransformCommand() + @Override + public String version() { - RuntimeExec runtimeExec = new RuntimeExec(); - Map commandsAndArguments = new HashMap<>(); - commandsAndArguments.put(".*", new String[]{EXE, "${source}", "SPLIT:${options}", "-strip", "-quiet", "${target}"}); - runtimeExec.setCommandsAndArguments(commandsAndArguments); - - Map processProperties = new HashMap<>(); - processProperties.put("MAGICK_HOME", ROOT); - processProperties.put("DYLD_FALLBACK_LIBRARY_PATH", DYN); - processProperties.put("LD_LIBRARY_PATH", DYN); - runtimeExec.setProcessProperties(processProperties); - - Map defaultProperties = new HashMap<>(); - defaultProperties.put("options", null); - runtimeExec.setDefaultProperties(defaultProperties); - - runtimeExec.setErrorCodes("1,2,255,400,405,410,415,420,425,430,435,440,450,455,460,465,470,475,480,485,490,495,499,700,705,710,715,720,725,730,735,740,750,755,760,765,770,775,780,785,790,795,799"); - - return runtimeExec; - } - - private static RuntimeExec createCheckCommand() - { - RuntimeExec runtimeExec = new RuntimeExec(); - Map commandsAndArguments = new HashMap<>(); - commandsAndArguments.put(".*", new String[]{EXE, "-version"}); - runtimeExec.setCommandsAndArguments(commandsAndArguments); - - return runtimeExec; + return commandExecutor.version(); } @Override - protected ProbeTestTransform getProbeTestTransform() + public ProbeTestTransform getProbeTestTransform() { // See the Javadoc on this method and Probes.md for the choice of these values. - return new ProbeTestTransform(this, "quick.jpg", "quick.png", - 35593, 1024, 150, 1024, 60*15+1,60*15+0) + return new ProbeTestTransform(this, logger, "quick.jpg", "quick.png", + 35593, 1024, 150, 1024, 60*15+1,60*15) { @Override protected void executeTransformCommand(File sourceFile, File targetFile) { - ImageMagickController.this.executeTransformCommand("", sourceFile, "", targetFile, null); + commandExecutor.run("", sourceFile, "", targetFile, null); } }; } @@ -168,6 +152,7 @@ public class ImageMagickController extends AbstractTransformerController { String targetFilename = createTargetFileName(sourceMultipartFile.getOriginalFilename(), targetExtension); + getProbeTestTransform().incrementTransformerCount(); File sourceFile = createSourceFile(request, sourceMultipartFile); File targetFile = createTargetFile(request, targetFilename); // Both files are deleted by TransformInterceptor.afterCompletion @@ -176,14 +161,20 @@ public class ImageMagickController extends AbstractTransformerController cropXOffset, cropYOffset, thumbnail, resizeWidth, resizeHeight, resizePercentage, allowEnlargement, maintainAspectRatio, commandOptions); String pageRange = calculatePageRange(startPage, endPage); - executeTransformCommand(options, sourceFile, pageRange, targetFile, timeout); + commandExecutor.run(options, sourceFile, pageRange, targetFile, + timeout); - return createAttachment(targetFilename, targetFile, testDelay); + final ResponseEntity body = createAttachment(targetFilename, targetFile); + LogEntry.setTargetSize(targetFile.length()); + long time = LogEntry.setStatusCodeAndMessage(200, "Success"); + time += LogEntry.addDelay(testDelay); + getProbeTestTransform().recordTransformTime(time); + return body; } @Override - protected void processTransform(File sourceFile, File targetFile, - Map transformOptions, Long timeout) + public void processTransform(final File sourceFile, final File targetFile, + final Map transformOptions, final Long timeout) { Integer startPage = stringToInteger(transformOptions.get("startPage")); Integer endPage = stringToInteger(transformOptions.get("endPage")); @@ -201,28 +192,17 @@ public class ImageMagickController extends AbstractTransformerController Boolean resizePercentage = stringToBoolean(transformOptions.get("resizePercentage")); Boolean allowEnlargement = stringToBoolean(transformOptions.get("allowEnlargement")); Boolean maintainAspectRatio = stringToBoolean(transformOptions.get("maintainAspectRatio")); - String commandOptions = transformOptions.get("commandOptions"); - String options = buildTransformOptions(startPage, endPage , alphaRemove, autoOrient, cropGravity, cropWidth, cropHeight, cropPercentage, - cropXOffset, cropYOffset, thumbnail, resizeWidth, resizeHeight, resizePercentage, allowEnlargement, maintainAspectRatio, commandOptions); - String pageRange = calculatePageRange(startPage, endPage); + final String options = buildTransformOptions(startPage, endPage, alphaRemove, autoOrient, + cropGravity, cropWidth, cropHeight, cropPercentage, + cropXOffset, cropYOffset, thumbnail, resizeWidth, resizeHeight, resizePercentage, allowEnlargement, maintainAspectRatio, null); + final String pageRange = calculatePageRange(startPage, endPage); - executeTransformCommand(options, sourceFile, pageRange, targetFile, timeout); + commandExecutor.run(options, sourceFile, pageRange, targetFile, + timeout); } - private void executeTransformCommand(String options, File sourceFile, String pageRange, File targetFile, Long timeout) - { - LogEntry.setOptions(pageRange+(pageRange.isEmpty() ? "" : " ")+options); - - Map properties = new HashMap(5); - properties.put("options", options); - properties.put("source", sourceFile.getAbsolutePath()+pageRange); - properties.put("target", targetFile.getAbsolutePath()); - - executeTransformCommand(properties, targetFile, timeout); - } - - private String buildTransformOptions(Integer startPage, Integer endPage, Boolean alphaRemove, + private static String buildTransformOptions(Integer startPage, Integer endPage, Boolean alphaRemove, Boolean autoOrient, String cropGravity, Integer cropWidth, Integer cropHeight, Boolean cropPercentage, Integer cropXOffset, Integer cropYOffset, Boolean thumbnail, Integer resizeWidth, Integer resizeHeight, Boolean resizePercentage, @@ -261,7 +241,7 @@ public class ImageMagickController extends AbstractTransformerController args.add(cropGravity); } - StringBuilder crop = new StringBuilder(""); + StringBuilder crop = new StringBuilder(); if (cropWidth != null && cropWidth >= 0) { crop.append(cropWidth); @@ -303,7 +283,7 @@ public class ImageMagickController extends AbstractTransformerController if (resizeHeight != null || resizeWidth != null || resizePercentage !=null || maintainAspectRatio != null) { args.add(thumbnail != null && thumbnail ? "-thumbnail" : "-resize"); - StringBuilder resize = new StringBuilder(""); + StringBuilder resize = new StringBuilder(); if (resizeWidth != null && resizeWidth >= 0) { resize.append(resizeWidth); @@ -335,10 +315,9 @@ public class ImageMagickController extends AbstractTransformerController args.toString(); } - private String calculatePageRange(Integer startPage, Integer endPage) + private static String calculatePageRange(Integer startPage, Integer endPage) { - return - startPage == null + return startPage == null ? endPage == null ? "" : "["+endPage+']' diff --git a/alfresco-docker-imagemagick/src/main/java/org/alfresco/transformer/executors/ImageMagickCommandExecutor.java b/alfresco-docker-imagemagick/src/main/java/org/alfresco/transformer/executors/ImageMagickCommandExecutor.java new file mode 100644 index 00000000..fd9ab90c --- /dev/null +++ b/alfresco-docker-imagemagick/src/main/java/org/alfresco/transformer/executors/ImageMagickCommandExecutor.java @@ -0,0 +1,52 @@ +package org.alfresco.transformer.executors; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.util.exec.RuntimeExec; +import org.springframework.stereotype.Component; + +/** + */ +@Component +public class ImageMagickCommandExecutor extends AbstractCommandExecutor +{ + private static final String ROOT = "/usr/lib64/ImageMagick-7.0.7"; + private static final String DYN = ROOT + "/lib"; + private static final String EXE = "/usr/bin/convert"; + + @Override + protected RuntimeExec createTransformCommand() + { + RuntimeExec runtimeExec = new RuntimeExec(); + Map commandsAndArguments = new HashMap<>(); + commandsAndArguments.put(".*", + new String[]{EXE, "${source}", "SPLIT:${options}", "-strip", "-quiet", "${target}"}); + runtimeExec.setCommandsAndArguments(commandsAndArguments); + + Map processProperties = new HashMap<>(); + processProperties.put("MAGICK_HOME", ROOT); + processProperties.put("DYLD_FALLBACK_LIBRARY_PATH", DYN); + processProperties.put("LD_LIBRARY_PATH", DYN); + runtimeExec.setProcessProperties(processProperties); + + Map defaultProperties = new HashMap<>(); + defaultProperties.put("options", null); + runtimeExec.setDefaultProperties(defaultProperties); + + runtimeExec.setErrorCodes( + "1,2,255,400,405,410,415,420,425,430,435,440,450,455,460,465,470,475,480,485,490,495,499,700,705,710,715,720,725,730,735,740,750,755,760,765,770,775,780,785,790,795,799"); + + return runtimeExec; + } + + @Override + protected RuntimeExec createCheckCommand() + { + RuntimeExec runtimeExec = new RuntimeExec(); + Map commandsAndArguments = new HashMap<>(); + commandsAndArguments.put(".*", new String[]{EXE, "-version"}); + runtimeExec.setCommandsAndArguments(commandsAndArguments); + return runtimeExec; + } +} diff --git a/alfresco-docker-imagemagick/src/test/java/org/alfresco/transformer/ImageMagickControllerTest.java b/alfresco-docker-imagemagick/src/test/java/org/alfresco/transformer/ImageMagickControllerTest.java index 71e61fd2..11e5ef17 100644 --- a/alfresco-docker-imagemagick/src/test/java/org/alfresco/transformer/ImageMagickControllerTest.java +++ b/alfresco-docker-imagemagick/src/test/java/org/alfresco/transformer/ImageMagickControllerTest.java @@ -25,21 +25,48 @@ */ package org.alfresco.transformer; +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; 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; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import org.alfresco.transform.client.model.TransformReply; import org.alfresco.transform.client.model.TransformRequest; +import org.alfresco.transformer.executors.ImageMagickCommandExecutor; +import org.alfresco.transformer.model.FileRefEntity; +import org.alfresco.transformer.model.FileRefResponse; +import org.alfresco.util.exec.RuntimeExec; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.stubbing.Answer; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.util.StringUtils; /** * Test the ImageMagickController without a server. @@ -49,16 +76,104 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @WebMvcTest(ImageMagickController.class) public class ImageMagickControllerTest extends AbstractTransformerControllerTest { + @Mock + private RuntimeExec.ExecutionResult mockExecutionResult; + + @Mock + private RuntimeExec mockTransformCommand; + + @Mock + private RuntimeExec mockCheckCommand; + + @SpyBean + private ImageMagickCommandExecutor commandExecutor; + @SpyBean private ImageMagickController controller; @Before public void before() throws IOException { - controller.setAlfrescoSharedFileStoreClient(alfrescoSharedFileStoreClient); - super.controller = controller; + commandExecutor.setTransformCommand(mockTransformCommand); + commandExecutor.setCheckCommand(mockCheckCommand); - super.mockTransformCommand(controller, "jpg", "png", "image/jpg", true); + mockTransformCommand("jpg", "png", "image/jpg", true); + } + + @Override + protected void mockTransformCommand(String sourceExtension, + String targetExtension, String sourceMimetype, + boolean readTargetFileBytes) throws IOException + { + this.sourceExtension = sourceExtension; + this.targetExtension = targetExtension; + this.sourceMimetype = sourceMimetype; + + expectedOptions = null; + expectedSourceSuffix = null; + expectedSourceFileBytes = readTestFile(sourceExtension); + expectedTargetFileBytes = readTargetFileBytes ? readTestFile(targetExtension) : null; + sourceFile = new MockMultipartFile("file", "quick." + sourceExtension, sourceMimetype, expectedSourceFileBytes); + + when(mockTransformCommand.execute(any(), anyLong())).thenAnswer( + (Answer) invocation -> { + Map actualProperties = invocation.getArgument(0); + assertEquals("There should be 3 properties", 3, actualProperties.size()); + + String actualOptions = actualProperties.get("options"); + String actualSource = actualProperties.get("source"); + String actualTarget = actualProperties.get("target"); + String actualTargetExtension = StringUtils.getFilenameExtension(actualTarget); + + assertNotNull(actualSource); + assertNotNull(actualTarget); + if (expectedSourceSuffix != null) + { + assertTrue("The source file \""+actualSource+"\" should have ended in \""+expectedSourceSuffix+"\"", actualSource.endsWith(expectedSourceSuffix)); + actualSource = actualSource.substring(0, actualSource.length()-expectedSourceSuffix.length()); + } + + assertNotNull(actualOptions); + if (expectedOptions != null) + { + assertEquals("expectedOptions", expectedOptions, actualOptions); + } + + Long actualTimeout = invocation.getArgument(1); + assertNotNull(actualTimeout); + if (expectedTimeout != null) + { + assertEquals("expectedTimeout", expectedTimeout, actualTimeout); + } + + // Copy a test file into the target file location if it exists + int i = actualTarget.lastIndexOf('_'); + if (i >= 0) + { + String testFilename = actualTarget.substring(i+1); + File testFile = getTestFile(testFilename, false); + File targetFile = new File(actualTarget); + generateTargetFileFromResourceFile(actualTargetExtension, testFile, + targetFile); + } + + // Check the supplied source file has not been changed. + byte[] actualSourceFileBytes = Files.readAllBytes(new File(actualSource).toPath()); + assertTrue("Source file is not the same", Arrays.equals(expectedSourceFileBytes, actualSourceFileBytes)); + + return mockExecutionResult; + }); + + when(mockExecutionResult.getExitValue()).thenReturn(0); + when(mockExecutionResult.getStdErr()).thenReturn("STDERROR"); + when(mockExecutionResult.getStdOut()).thenReturn("STDOUT"); + } + + + @Override + protected AbstractTransformerController getController() + { + return controller; } @Test @@ -67,7 +182,7 @@ public class ImageMagickControllerTest extends AbstractTransformerControllerTest for (String value: new String[] {"North", "NorthEast", "East", "SouthEast", "South", "SouthWest", "West", "NorthWest", "Center"}) { expectedOptions = "-gravity "+value+" +repage"; - mockMvc.perform(MockMvcRequestBuilders.fileUpload("/transform") + mockMvc.perform(MockMvcRequestBuilders.multipart("/transform") .file(sourceFile) .param("targetExtension", targetExtension) .param("cropGravity", value)) @@ -80,7 +195,7 @@ public class ImageMagickControllerTest extends AbstractTransformerControllerTest @Test public void cropGravityBadTest() throws Exception { - mockMvc.perform(MockMvcRequestBuilders.fileUpload("/transform") + mockMvc.perform(MockMvcRequestBuilders.multipart("/transform") .file(sourceFile) .param("targetExtension", targetExtension) .param("cropGravity", "badValue")) @@ -92,7 +207,7 @@ public class ImageMagickControllerTest extends AbstractTransformerControllerTest { expectedOptions = "-alpha remove -gravity SouthEast -crop 123x456%+90+12 +repage -thumbnail 321x654%!"; expectedSourceSuffix = "[2-3]"; - mockMvc.perform(MockMvcRequestBuilders.fileUpload("/transform") + mockMvc.perform(MockMvcRequestBuilders.multipart("/transform") .file(sourceFile) .param("targetExtension", targetExtension) @@ -126,7 +241,7 @@ public class ImageMagickControllerTest extends AbstractTransformerControllerTest { expectedOptions = "-auto-orient -gravity SouthEast -crop 123x456+90+12 +repage -resize 321x654>"; expectedSourceSuffix = "[2-3]"; - mockMvc.perform(MockMvcRequestBuilders.fileUpload("/transform") + mockMvc.perform(MockMvcRequestBuilders.multipart("/transform") .file(sourceFile) .param("targetExtension", targetExtension) @@ -160,7 +275,7 @@ public class ImageMagickControllerTest extends AbstractTransformerControllerTest { // Example of why the commandOptions parameter is a bad idea. expectedOptions = "( horrible command / ); -resize 321x654>"; - mockMvc.perform(MockMvcRequestBuilders.fileUpload("/transform") + mockMvc.perform(MockMvcRequestBuilders.multipart("/transform") .file(sourceFile) .param("targetExtension", targetExtension) .param("thumbnail", "false") @@ -180,4 +295,64 @@ public class ImageMagickControllerTest extends AbstractTransformerControllerTest transformRequest.setSourceMediaType(MediaType.IMAGE_PNG_VALUE); transformRequest.setTargetMediaType(MediaType.IMAGE_PNG_VALUE); } + + @Test + public void badExitCodeTest() throws Exception + { + when(mockExecutionResult.getExitValue()).thenReturn(1); + + mockMvc.perform(mockMvcRequest("/transform", sourceFile, "targetExtension", "xxx")) + .andExpect(status().is(400)) + .andExpect(status().reason(containsString("Transformer exit code was not 0: \nSTDERR"))); + } + + @Test + public void testPojoTransform() throws Exception + { + // Files + String sourceFileRef = UUID.randomUUID().toString(); + File sourceFile = getTestFile("quick." + sourceExtension, true); + String targetFileRef = UUID.randomUUID().toString(); + + + // Transformation Request POJO + TransformRequest transformRequest = new TransformRequest(); + transformRequest.setRequestId("1"); + transformRequest.setSchema(1); + transformRequest.setClientData("Alfresco Digital Business Platform"); + transformRequest.setTransformRequestOptions(new HashMap<>()); + transformRequest.setSourceReference(sourceFileRef); + transformRequest.setSourceExtension(sourceExtension); + transformRequest.setSourceSize(sourceFile.length()); + transformRequest.setTargetExtension(targetExtension); + + // HTTP Request + HttpHeaders headers = new HttpHeaders(); + headers.set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=quick." + sourceExtension); + ResponseEntity response = new ResponseEntity<>(new FileSystemResource( + sourceFile), headers, HttpStatus.OK); + + when(alfrescoSharedFileStoreClient.retrieveFile(sourceFileRef)).thenReturn(response); + when(alfrescoSharedFileStoreClient.saveFile(any())).thenReturn(new FileRefResponse(new FileRefEntity(targetFileRef))); + when(mockExecutionResult.getExitValue()).thenReturn(0); + + // 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("/transform") + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).content(tr)) + .andExpect(status().is(HttpStatus.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()); + } + } diff --git a/alfresco-docker-imagemagick/src/test/java/org/alfresco/transformer/ImageMagickHttpRequestTest.java b/alfresco-docker-imagemagick/src/test/java/org/alfresco/transformer/ImageMagickHttpRequestTest.java index 983c8076..4bbb0077 100644 --- a/alfresco-docker-imagemagick/src/test/java/org/alfresco/transformer/ImageMagickHttpRequestTest.java +++ b/alfresco-docker-imagemagick/src/test/java/org/alfresco/transformer/ImageMagickHttpRequestTest.java @@ -25,7 +25,6 @@ */ package org.alfresco.transformer; -import org.alfresco.transformer.AbstractHttpRequestTest; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; @@ -48,5 +47,5 @@ public class ImageMagickHttpRequestTest extends AbstractHttpRequestTest protected String getSourceExtension() { return "jpg"; - }; + } } diff --git a/alfresco-docker-libreoffice/src/main/java/org/alfresco/transformer/Application.java b/alfresco-docker-libreoffice/src/main/java/org/alfresco/transformer/Application.java index ec500713..87752c5e 100644 --- a/alfresco-docker-libreoffice/src/main/java/org/alfresco/transformer/Application.java +++ b/alfresco-docker-libreoffice/src/main/java/org/alfresco/transformer/Application.java @@ -12,6 +12,8 @@ package org.alfresco.transformer; import io.micrometer.core.instrument.MeterRegistry; + +import org.alfresco.transformer.executors.LibreOfficeJavaExecutor; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; @@ -27,9 +29,16 @@ public class Application @Value("${container.name}") private String containerName; - @Bean MeterRegistryCustomizer metricsCommonTags() { + @Bean + MeterRegistryCustomizer metricsCommonTags() { return registry -> registry.config().commonTags("containerName", containerName); } + + @Bean + public LibreOfficeJavaExecutor javaExecutor() + { + return new LibreOfficeJavaExecutor(); + } public static void main(String[] args) { diff --git a/alfresco-docker-libreoffice/src/main/java/org/alfresco/transformer/LibreOfficeController.java b/alfresco-docker-libreoffice/src/main/java/org/alfresco/transformer/LibreOfficeController.java index d6f7feeb..a6351ab3 100644 --- a/alfresco-docker-libreoffice/src/main/java/org/alfresco/transformer/LibreOfficeController.java +++ b/alfresco-docker-libreoffice/src/main/java/org/alfresco/transformer/LibreOfficeController.java @@ -11,19 +11,23 @@ */ package org.alfresco.transformer; +import static org.alfresco.transformer.fs.FileManager.createAttachment; +import static org.alfresco.transformer.fs.FileManager.createSourceFile; +import static org.alfresco.transformer.fs.FileManager.createTargetFile; +import static org.alfresco.transformer.fs.FileManager.createTargetFileName; +import static org.alfresco.transformer.logging.StandardMessages.ENTERPRISE_LICENCE; + import java.io.File; -import java.io.IOException; +import java.util.Arrays; import java.util.Map; import javax.servlet.http.HttpServletRequest; +import org.alfresco.transformer.executors.LibreOfficeJavaExecutor; +import org.alfresco.transformer.logging.LogEntry; +import org.alfresco.transformer.probes.ProbeTestTransform; +import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.PDPageContentStream; -import org.artofsolving.jodconverter.OfficeDocumentConverter; -import org.artofsolving.jodconverter.office.OfficeException; -import org.artofsolving.jodconverter.office.OfficeManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; import org.springframework.http.MediaType; @@ -33,8 +37,6 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; -import com.sun.star.task.ErrorCodeIOException; - /** * Controller for the Docker based LibreOffice transformer. * @@ -59,86 +61,48 @@ import com.sun.star.task.ErrorCodeIOException; @Controller public class LibreOfficeController extends AbstractTransformerController { - private static final String OFFICE_HOME = "/opt/libreoffice5.4"; - - private static final int JODCONVERTER_TRANSFORMATION_ERROR_CODE = 3088; - - private JodConverter jodconverter; + private static final Log logger = LogFactory.getLog(LibreOfficeController.class); @Autowired - public LibreOfficeController() throws Exception + private LibreOfficeJavaExecutor javaExecutor; + + @Autowired + public LibreOfficeController() { - logger = LogFactory.getLog(LibreOfficeController.class); logger.info("-------------------------------------------------------------------------------------------------------------------------------------------------------"); - logEnterpriseLicenseMessage(); + Arrays.stream(ENTERPRISE_LICENCE.split("\\n")).forEach(logger::info); logger.info("This transformer uses LibreOffice from The Document Foundation. See the license at https://www.libreoffice.org/download/license/ or in /libreoffice.txt"); logger.info("-------------------------------------------------------------------------------------------------------------------------------------------------------"); } - private static JodConverter createJodConverter(Long taskExecutionTimeout) - { - String timeout = taskExecutionTimeout == null || taskExecutionTimeout <= 0 ? "120000" : taskExecutionTimeout.toString(); - - JodConverterSharedInstance jodconverter = new JodConverterSharedInstance(); - - jodconverter.setOfficeHome(OFFICE_HOME); // jodconverter.officeHome - jodconverter.setMaxTasksPerProcess("200"); // jodconverter.maxTasksPerProcess - jodconverter.setTaskExecutionTimeout(timeout); // jodconverter.maxTaskExecutionTimeout - jodconverter.setTaskQueueTimeout("30000"); // jodconverter.taskQueueTimeout - jodconverter.setConnectTimeout("28000"); // jodconverter.connectTimeout - jodconverter.setPortNumbers("8100"); // jodconverter.portNumbers - jodconverter.setTemplateProfileDir(""); // jodconverter.templateProfileDir - jodconverter.setEnabled("true"); // jodconverter.enabled - jodconverter.afterPropertiesSet(); - - return jodconverter; - } - - public void setJodConverter(JodConverter jodconverter) - { - this.jodconverter = jodconverter; - } - - /** - * Jodconverter timeouts are per OfficeManager, so we would need multiple OfficeManagers if we - * have different timeouts. Alfresco only has one. So we delay building it until the first request. - * This was not done previously. - */ - private synchronized void setJodConverterOnFirstRequest(Long timeout) - { - if (jodconverter == null) - { - setJodConverter(createJodConverter(timeout)); - } - } - @Override - protected String getTransformerName() + public String getTransformerName() { return "LibreOffice"; } @Override - protected String version() + public String version() { return "LibreOffice available"; } @Override - protected ProbeTestTransform getProbeTestTransform() + public ProbeTestTransform getProbeTestTransform() { // See the Javadoc on this method and Probes.md for the choice of these values. - return new ProbeTestTransform(this, "quick.doc", "quick.pdf", + return new ProbeTestTransform(this, logger, "quick.doc", "quick.pdf", 11817, 1024, 150, 10240, 60*30+1, 60*15+20) { @Override protected void executeTransformCommand(File sourceFile, File targetFile) { - LibreOfficeController.this.executeTransformCommand(sourceFile, targetFile, null); + javaExecutor.call(sourceFile, targetFile); } }; } + //todo: the "timeout" request parameter is ignored; the timeout is preset at JodConverter creation @PostMapping(value = "/transform", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity transform(HttpServletRequest request, @RequestParam("file") MultipartFile sourceMultipartFile, @@ -147,122 +111,25 @@ public class LibreOfficeController extends AbstractTransformerController @RequestParam(value = "testDelay", required = false) Long testDelay) { String targetFilename = createTargetFileName(sourceMultipartFile.getOriginalFilename(), targetExtension); + getProbeTestTransform().incrementTransformerCount(); File sourceFile = createSourceFile(request, sourceMultipartFile); File targetFile = createTargetFile(request, targetFilename); // Both files are deleted by TransformInterceptor.afterCompletion - executeTransformCommand(sourceFile, targetFile, timeout); + javaExecutor.call(sourceFile, targetFile); - return createAttachment(targetFilename, targetFile, testDelay); + final ResponseEntity body = createAttachment(targetFilename, targetFile); + LogEntry.setTargetSize(targetFile.length()); + long time = LogEntry.setStatusCodeAndMessage(200, "Success"); + time += LogEntry.addDelay(testDelay); + getProbeTestTransform().recordTransformTime(time); + return body; } @Override - protected void processTransform(File sourceFile, File targetFile, + public void processTransform(File sourceFile, File targetFile, Map transformOptions, Long timeout) { - executeTransformCommand(sourceFile, targetFile, timeout); - } - - protected void executeTransformCommand(File sourceFile, File targetFile, Long timeout) - { - timeout = timeout != null && timeout > 0 ? timeout : 0; - - try - { - convert(sourceFile, targetFile, timeout); - } - catch (OfficeException e) - { - throw new TransformException(400, "LibreOffice server conversion failed: \n"+ - " from file: " + sourceFile + "\n" + - " to file: " + targetFile, - e); - } - catch (Throwable throwable) - { - // Because of the known bug with empty Spreadsheets in JodConverter try to catch exception and produce empty pdf file - if (throwable.getCause() instanceof ErrorCodeIOException && - ((ErrorCodeIOException) throwable.getCause()).ErrCode == JODCONVERTER_TRANSFORMATION_ERROR_CODE) - { - logger.warn("Transformation failed: \n" + - "from file: " + sourceFile + "\n" + - "to file: " + targetFile + - "Source file " + sourceFile + " has no content"); - produceEmptyPdfFile(targetFile); - } - else - { - throw throwable; - } - } - - if (!targetFile.exists() || targetFile.length() == 0L) - { - throw new TransformException(500, "Transformer failed to create an output file"); - } - } - - void convert(File sourceFile, File targetFile, long timeout) - { - setJodConverterOnFirstRequest(timeout); - OfficeManager officeManager = jodconverter.getOfficeManager(); - OfficeDocumentConverter converter = new OfficeDocumentConverter(officeManager); - converter.convert(sourceFile, targetFile); - } - - /** - * This method produces an empty PDF file at the specified File location. - * Apache's PDFBox is used to create the PDF file. - */ - private void produceEmptyPdfFile(File targetFile) - { - // If improvement PDFBOX-914 is incorporated, we can do this with a straight call to - // org.apache.pdfbox.TextToPdf.createPDFFromText(new StringReader("")); - // https://issues.apache.org/jira/browse/PDFBOX-914 - - PDDocument pdfDoc = null; - PDPageContentStream contentStream = null; - try - { - pdfDoc = new PDDocument(); - PDPage pdfPage = new PDPage(); - // Even though, we want an empty PDF, some libs (e.g. PDFRenderer) object to PDFs - // that have literally nothing in them. So we'll put a content stream in it. - contentStream = new PDPageContentStream(pdfDoc, pdfPage); - pdfDoc.addPage(pdfPage); - - // Now write the in-memory PDF document into the temporary file. - pdfDoc.save(targetFile.getAbsolutePath()); - - } - catch (IOException iox) - { - throw new TransformException(500, "Error creating empty PDF file", iox); - } - finally - { - if (contentStream != null) - { - try - { - contentStream.close(); - } - catch (IOException ignored) - { - // Intentionally empty - } - } - if (pdfDoc != null) - { - try - { - pdfDoc.close(); - } - catch (IOException ignored) - { - // Intentionally empty. - } - } - } + javaExecutor.call(sourceFile, targetFile); } } diff --git a/alfresco-docker-libreoffice/src/main/java/org/alfresco/transformer/JodConverter.java b/alfresco-docker-libreoffice/src/main/java/org/alfresco/transformer/executors/JodConverter.java similarity index 76% rename from alfresco-docker-libreoffice/src/main/java/org/alfresco/transformer/JodConverter.java rename to alfresco-docker-libreoffice/src/main/java/org/alfresco/transformer/executors/JodConverter.java index 2ac68ba7..7952310f 100644 --- a/alfresco-docker-libreoffice/src/main/java/org/alfresco/transformer/JodConverter.java +++ b/alfresco-docker-libreoffice/src/main/java/org/alfresco/transformer/executors/JodConverter.java @@ -9,11 +9,11 @@ * agreement is prohibited. * #L% */ -package org.alfresco.transformer; +package org.alfresco.transformer.executors; import org.artofsolving.jodconverter.office.OfficeManager; -///////// THIS FILE IS A COPY OF THE CODE IN alfresco-repository ///////////// +///////// THIS FILE WAS A COPY OF THE CODE IN alfresco-repository ///////////// public interface JodConverter { @@ -21,11 +21,11 @@ public interface JodConverter * Gets the JodConverter OfficeManager. * @return */ - public abstract OfficeManager getOfficeManager(); + OfficeManager getOfficeManager(); /** * This method returns a boolean indicating whether the JodConverter connection to OOo is available. * @return true if available, else false */ - public abstract boolean isAvailable(); + boolean isAvailable(); } diff --git a/alfresco-docker-libreoffice/src/main/java/org/alfresco/transformer/JodConverterSharedInstance.java b/alfresco-docker-libreoffice/src/main/java/org/alfresco/transformer/executors/JodConverterSharedInstance.java similarity index 90% rename from alfresco-docker-libreoffice/src/main/java/org/alfresco/transformer/JodConverterSharedInstance.java rename to alfresco-docker-libreoffice/src/main/java/org/alfresco/transformer/executors/JodConverterSharedInstance.java index 2b9c1023..b22ea339 100644 --- a/alfresco-docker-libreoffice/src/main/java/org/alfresco/transformer/JodConverterSharedInstance.java +++ b/alfresco-docker-libreoffice/src/main/java/org/alfresco/transformer/executors/JodConverterSharedInstance.java @@ -23,12 +23,11 @@ * along with Alfresco. If not, see . * #L% */ -package org.alfresco.transformer; +package org.alfresco.transformer.executors; import java.io.File; -import java.io.FileFilter; -import java.io.FilenameFilter; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.StringTokenizer; @@ -41,7 +40,7 @@ import org.artofsolving.jodconverter.office.OfficeManager; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; -///////// THIS FILE IS A COPY OF THE CODE IN alfresco-repository ///////////// +///////// THIS FILE WAS A COPY OF THE CODE IN alfresco-repository ///////////// /** * Makes use of the JodConverter library and an installed @@ -51,10 +50,10 @@ import org.springframework.beans.factory.InitializingBean; */ public class JodConverterSharedInstance implements InitializingBean, DisposableBean, JodConverter { - private static Log logger = LogFactory.getLog(JodConverterSharedInstance.class); + private static final Log logger = LogFactory.getLog(JodConverterSharedInstance.class); private OfficeManager officeManager; - boolean isAvailable = false; + private boolean isAvailable = false; // JodConverter's built-in configuration settings. // @@ -82,7 +81,7 @@ public class JodConverterSharedInstance implements InitializingBean, DisposableB private Boolean deprecatedOooEnabled; private int[] deprecatedOooPortNumbers; - public void setMaxTasksPerProcess(String maxTasksPerProcess) + void setMaxTasksPerProcess(String maxTasksPerProcess) { Long l = parseStringForLong(maxTasksPerProcess.trim()); if (l != null) @@ -96,7 +95,7 @@ public class JodConverterSharedInstance implements InitializingBean, DisposableB this.url = url; } - public void setOfficeHome(String officeHome) + void setOfficeHome(String officeHome) { this.officeHome = officeHome == null ? "" : officeHome.trim(); } @@ -106,7 +105,7 @@ public class JodConverterSharedInstance implements InitializingBean, DisposableB this.deprecatedOooExe = deprecatedOooExe == null ? "" : deprecatedOooExe.trim(); } - public void setPortNumbers(String s) + void setPortNumbers(String s) { portNumbers = parsePortNumbers(s, "jodconverter"); } @@ -147,12 +146,12 @@ public class JodConverterSharedInstance implements InitializingBean, DisposableB return portNumbers; } - public void setTaskExecutionTimeout(String taskExecutionTimeout) + void setTaskExecutionTimeout(String taskExecutionTimeout) { this.taskExecutionTimeout = parseStringForLong(taskExecutionTimeout.trim()); } - public void setTemplateProfileDir(String templateProfileDir) + void setTemplateProfileDir(String templateProfileDir) { if (templateProfileDir == null || templateProfileDir.trim().length() == 0) { @@ -169,26 +168,26 @@ public class JodConverterSharedInstance implements InitializingBean, DisposableB } } - public void setTaskQueueTimeout(String taskQueueTimeout) + void setTaskQueueTimeout(String taskQueueTimeout) { this.taskQueueTimeout = parseStringForLong(taskQueueTimeout.trim()); } - public void setConnectTimeout(String connectTimeout) + void setConnectTimeout(String connectTimeout) { this.connectTimeout = parseStringForLong(connectTimeout.trim()); } - public void setEnabled(String enabled) + void setEnabled(final String enabledStr) { - this.enabled = parseEnabled(enabled); + enabled = parseEnabled(enabledStr); // If this is a request from the Enterprise Admin console to disable the JodConverter. - if (this.enabled == false && (deprecatedOooEnabled == null || deprecatedOooEnabled == false)) + if (!enabled && (deprecatedOooEnabled == null || !deprecatedOooEnabled)) { // We need to change isAvailable to false so we don't make calls to a previously started OfficeManger. // In the case of Enterprise it is very unlikely that ooo.enabled will have been set to true. - this.isAvailable = false; + isAvailable = false; } } @@ -207,7 +206,7 @@ public class JodConverterSharedInstance implements InitializingBean, DisposableB // So that Community systems <= Alfresco 6.0.1-ea keep working on upgrade, we may need to use the deprecated // ooo.exe setting rather than the jodconverter.officeHome setting if we don't have the jod setting as // oooDirect was replaced by jodconverter after this release. - String getOfficeHome() + private String getOfficeHome() { String officeHome = this.officeHome; if ((officeHome == null || officeHome.isEmpty()) && (deprecatedOooExe != null && !deprecatedOooExe.isEmpty())) @@ -243,7 +242,7 @@ public class JodConverterSharedInstance implements InitializingBean, DisposableB // Community set properties via alfresco-global.properties. // Enterprise may do the same but may also reset jodconverter.enabled them via the Admin console. // In the case of Enterprise it is very unlikely that ooo.enabled will be set to true. - boolean isEnabled() + private boolean isEnabled() { return (deprecatedOooEnabled != null && deprecatedOooEnabled) || (enabled != null && enabled); } @@ -251,7 +250,7 @@ public class JodConverterSharedInstance implements InitializingBean, DisposableB // So that Community systems <= Alfresco 6.0.1-ea keep working on upgrade, we may need to use the deprecated // ooo.port setting rather than the jodconverter.portNumbers if ooo.enabled is true and jodconverter.enabled // is false. - int[] getPortNumbers() + private int[] getPortNumbers() { return (enabled == null || !enabled) && deprecatedOooEnabled != null && deprecatedOooEnabled ? deprecatedOooPortNumbers @@ -260,11 +259,9 @@ public class JodConverterSharedInstance implements InitializingBean, DisposableB private Long parseStringForLong(String string) { - Long result = null; try { - long l = Long.parseLong(string); - result = new Long(l); + return Long.parseLong(string); } catch (NumberFormatException nfe) { @@ -272,9 +269,8 @@ public class JodConverterSharedInstance implements InitializingBean, DisposableB { logger.debug("Cannot parse numerical value from " + string); } - // else intentionally empty } - return result; + return null; } /* @@ -283,14 +279,14 @@ public class JodConverterSharedInstance implements InitializingBean, DisposableB */ public boolean isAvailable() { - final boolean result = isAvailable && (officeManager != null || (url != null && !url.isEmpty())); - return result; + return isAvailable && (officeManager != null || (url != null && !url.isEmpty())); } /* * (non-Javadoc) * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() */ + @Override public void afterPropertiesSet() { // isAvailable defaults to false afterPropertiesSet. It only becomes true on successful completion of this method. @@ -318,7 +314,7 @@ public class JodConverterSharedInstance implements InitializingBean, DisposableB } // Only start the JodConverter instance(s) if the subsystem is enabled. - if (isEnabled() == false) + if (!isEnabled()) { return; } @@ -418,7 +414,7 @@ public class JodConverterSharedInstance implements InitializingBean, DisposableB private void logAllSofficeFilesUnderOfficeHome() { - if (logger.isDebugEnabled() == false) + if (!logger.isDebugEnabled()) { return; } @@ -430,7 +426,7 @@ public class JodConverterSharedInstance implements InitializingBean, DisposableB logFileInfo(requestedOfficeHome); - for (File f : findSofficePrograms(requestedOfficeHome, new ArrayList(), 2)) + for (File f : findSofficePrograms(requestedOfficeHome, new ArrayList<>(), 2)) { logFileInfo(f); } @@ -449,26 +445,11 @@ public class JodConverterSharedInstance implements InitializingBean, DisposableB return results; } - File[] matchingFiles = searchRoot.listFiles(new FilenameFilter() - { - @Override - public boolean accept(File dir, String name) - { - return name.startsWith("soffice"); - } - }); - for (File f : matchingFiles) - { - results.add(f); - } + File[] matchingFiles = searchRoot.listFiles((dir, name) -> name.startsWith("soffice")); + Arrays.stream(matchingFiles) + .forEach(results::add); - for (File dir : searchRoot.listFiles(new FileFilter() - { - @Override - public boolean accept(File f) { - return f.isDirectory(); - } - })) + for (File dir : searchRoot.listFiles(File::isDirectory)) { findSofficePrograms(dir, results, currentRecursionDepth + 1, maxRecursionDepth); } @@ -482,7 +463,7 @@ public class JodConverterSharedInstance implements InitializingBean, DisposableB */ private void logFileInfo(File f) { - if (logger.isDebugEnabled() == false) + if (!logger.isDebugEnabled()) { return; } @@ -512,7 +493,9 @@ public class JodConverterSharedInstance implements InitializingBean, DisposableB * (non-Javadoc) * @see org.springframework.beans.factory.DisposableBean#destroy() */ - public void destroy() throws Exception { + @Override + public void destroy() + { this.isAvailable = false; if (officeManager != null) { @@ -530,6 +513,7 @@ public class JodConverterSharedInstance implements InitializingBean, DisposableB /* (non-Javadoc) * @see org.alfresco.repo.content.JodConverterWorker#getOfficeManager() */ + @Override public OfficeManager getOfficeManager() { return officeManager; diff --git a/alfresco-docker-libreoffice/src/main/java/org/alfresco/transformer/executors/LibreOfficeJavaExecutor.java b/alfresco-docker-libreoffice/src/main/java/org/alfresco/transformer/executors/LibreOfficeJavaExecutor.java new file mode 100644 index 00000000..ad3f794d --- /dev/null +++ b/alfresco-docker-libreoffice/src/main/java/org/alfresco/transformer/executors/LibreOfficeJavaExecutor.java @@ -0,0 +1,118 @@ +package org.alfresco.transformer.executors; + +import java.io.File; +import java.io.IOException; + +import org.alfresco.transformer.exceptions.TransformException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.artofsolving.jodconverter.OfficeDocumentConverter; +import org.artofsolving.jodconverter.office.OfficeException; +import org.artofsolving.jodconverter.office.OfficeManager; +import org.springframework.stereotype.Component; + +import com.sun.star.task.ErrorCodeIOException; + +@Component +public class LibreOfficeJavaExecutor implements JavaExecutor +{ + private static final Log logger = LogFactory.getLog(LibreOfficeJavaExecutor.class); + + private static final int JODCONVERTER_TRANSFORMATION_ERROR_CODE = 3088; + private static final String OFFICE_HOME = "/opt/libreoffice5.4"; + + private JodConverter jodconverter = createJodConverter(); + + private static JodConverter createJodConverter() + { + final String timeout = "120000"; + + JodConverterSharedInstance jodconverter = new JodConverterSharedInstance(); + + jodconverter.setOfficeHome(OFFICE_HOME); // jodconverter.officeHome + jodconverter.setMaxTasksPerProcess("200"); // jodconverter.maxTasksPerProcess + jodconverter.setTaskExecutionTimeout(timeout); // jodconverter.maxTaskExecutionTimeout + jodconverter.setTaskQueueTimeout("30000"); // jodconverter.taskQueueTimeout + jodconverter.setConnectTimeout("28000"); // jodconverter.connectTimeout + jodconverter.setPortNumbers("8100"); // jodconverter.portNumbers + jodconverter.setTemplateProfileDir(""); // jodconverter.templateProfileDir + jodconverter.setEnabled("true"); // jodconverter.enabled + jodconverter.afterPropertiesSet(); + + return jodconverter; + } + + @Override + public void call(File sourceFile, File targetFile, String... args) + { + try + { + convert(sourceFile, targetFile); + } + catch (OfficeException e) + { + throw new TransformException(400, "LibreOffice server conversion failed: \n" + + " from file: " + sourceFile + "\n" + + " to file: " + targetFile, e); + } + catch (Throwable throwable) + { + // Because of the known bug with empty Spreadsheets in JodConverter try to catch exception and produce empty pdf file + if (throwable.getCause() instanceof ErrorCodeIOException && + ((ErrorCodeIOException) throwable.getCause()).ErrCode == JODCONVERTER_TRANSFORMATION_ERROR_CODE) + { + logger.warn("Transformation failed: \n" + + "from file: " + sourceFile + "\n" + + "to file: " + targetFile + + "Source file " + sourceFile + " has no content"); + produceEmptyPdfFile(targetFile); + } + else + { + throw throwable; + } + } + + if (!targetFile.exists() || targetFile.length() == 0L) + { + throw new TransformException(500, "Transformer failed to create an output file"); + } + } + + public void convert(File sourceFile, File targetFile) + { + OfficeManager officeManager = jodconverter.getOfficeManager(); + OfficeDocumentConverter converter = new OfficeDocumentConverter(officeManager); + converter.convert(sourceFile, targetFile); + } + + /** + * This method produces an empty PDF file at the specified File location. + * Apache's PDFBox is used to create the PDF file. + */ + private static void produceEmptyPdfFile(File targetFile) + { + // If improvement PDFBOX-914 is incorporated, we can do this with a straight call to + // org.apache.pdfbox.TextToPdf.createPDFFromText(new StringReader("")); + // https://issues.apache.org/jira/browse/PDFBOX-914 + + PDPage pdfPage = new PDPage(); + try (PDDocument pdfDoc = new PDDocument(); + PDPageContentStream contentStream = new PDPageContentStream(pdfDoc, pdfPage)) + { + // Even though, we want an empty PDF, some libs (e.g. PDFRenderer) object to PDFs + // that have literally nothing in them. So we'll put a content stream in it. + pdfDoc.addPage(pdfPage); + + // Now write the in-memory PDF document into the temporary file. + pdfDoc.save(targetFile.getAbsolutePath()); + } + catch (IOException iox) + { + throw new TransformException(500, "Error creating empty PDF file", iox); + } + } +} diff --git a/alfresco-docker-libreoffice/src/test/java/org/alfresco/transformer/LibreOfficeControllerTest.java b/alfresco-docker-libreoffice/src/test/java/org/alfresco/transformer/LibreOfficeControllerTest.java index b88bb8f5..f646bd3b 100644 --- a/alfresco-docker-libreoffice/src/test/java/org/alfresco/transformer/LibreOfficeControllerTest.java +++ b/alfresco-docker-libreoffice/src/test/java/org/alfresco/transformer/LibreOfficeControllerTest.java @@ -29,26 +29,38 @@ import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyLong; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.util.Arrays; +import java.util.HashMap; +import java.util.UUID; +import org.alfresco.transform.client.model.TransformReply; import org.alfresco.transform.client.model.TransformRequest; +import org.alfresco.transformer.executors.LibreOfficeJavaExecutor; +import org.alfresco.transformer.model.FileRefEntity; +import org.alfresco.transformer.model.FileRefResponse; +import org.alfresco.util.exec.RuntimeExec; import org.artofsolving.jodconverter.office.OfficeException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.stubbing.Answer; +import org.mockito.Mock; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @@ -62,15 +74,18 @@ import org.springframework.util.StringUtils; @WebMvcTest(LibreOfficeControllerTest.class) public class LibreOfficeControllerTest extends AbstractTransformerControllerTest { + @Mock + private RuntimeExec.ExecutionResult mockExecutionResult; + + @SpyBean + private LibreOfficeJavaExecutor javaExecutor; + @SpyBean private LibreOfficeController controller; @Before public void before() throws IOException { - controller.setAlfrescoSharedFileStoreClient(alfrescoSharedFileStoreClient); - super.controller = controller; - sourceExtension = "doc"; targetExtension = "pdf"; sourceMimetype = "application/msword"; @@ -78,11 +93,11 @@ public class LibreOfficeControllerTest extends AbstractTransformerControllerTest // The following is based on super.mockTransformCommand(...) // This is because LibreOffice used JodConverter rather than a RuntimeExec - expectedSourceFileBytes = Files.readAllBytes(getTestFile("quick."+sourceExtension, true).toPath()); - expectedTargetFileBytes = Files.readAllBytes(getTestFile("quick."+targetExtension, true).toPath()); - sourceFile = new MockMultipartFile("file", "quick."+sourceExtension, sourceMimetype, expectedSourceFileBytes); + expectedSourceFileBytes = Files.readAllBytes(getTestFile("quick." + sourceExtension, true).toPath()); + expectedTargetFileBytes = Files.readAllBytes(getTestFile("quick." + targetExtension, true).toPath()); + sourceFile = new MockMultipartFile("file", "quick." + sourceExtension, sourceMimetype, expectedSourceFileBytes); - doAnswer((Answer) invocation -> + doAnswer(invocation -> { File sourceFile = invocation.getArgument(0); File targetFile = invocation.getArgument(1); @@ -91,19 +106,12 @@ public class LibreOfficeControllerTest extends AbstractTransformerControllerTest assertNotNull(sourceFile); assertNotNull(targetFile); - Long actualTimeout = invocation.getArgument(2); - assertNotNull(actualTimeout); - if (expectedTimeout != null) - { - assertEquals("expectedTimeout", expectedTimeout, actualTimeout); - } - // Copy a test file into the target file location if it exists String actualTarget = targetFile.getAbsolutePath(); int i = actualTarget.lastIndexOf('_'); if (i >= 0) { - String testFilename = actualTarget.substring(i+1); + String testFilename = actualTarget.substring(i + 1); File testFile = getTestFile(testFilename, false); generateTargetFileFromResourceFile(actualTargetExtension, testFile, targetFile); } @@ -113,20 +121,32 @@ public class LibreOfficeControllerTest extends AbstractTransformerControllerTest assertTrue("Source file is not the same", Arrays.equals(expectedSourceFileBytes, actualSourceFileBytes)); return null; - }).when(controller).convert(any(), any(), anyLong()); + }).when(javaExecutor).convert(any(), any()); + } + + @Override + protected void mockTransformCommand(String sourceExtension, String targetExtension, + String sourceMimetype, boolean readTargetFileBytes) + { + throw new IllegalStateException(); + } + + @Override + protected AbstractTransformerController getController() + { + return controller; } @Test - @Override public void badExitCodeTest() throws Exception { - doThrow(OfficeException.class).when(controller).convert(any(), any(), anyLong()); + doThrow(OfficeException.class).when(javaExecutor).convert(any(), any()); - mockMvc.perform(MockMvcRequestBuilders.fileUpload("/transform") - .file(sourceFile) - .param("targetExtension", "xxx")) - .andExpect(status().is(400)) - .andExpect(status().reason(containsString("LibreOffice - LibreOffice server conversion failed:"))); + mockMvc.perform(MockMvcRequestBuilders.multipart("/transform") + .file(sourceFile) + .param("targetExtension", "xxx")) + .andExpect(status().is(400)) + .andExpect(status().reason(containsString("LibreOffice - LibreOffice server conversion failed:"))); } @Override @@ -137,4 +157,58 @@ public class LibreOfficeControllerTest extends AbstractTransformerControllerTest transformRequest.setSourceMediaType("application/msword"); transformRequest.setTargetMediaType(MediaType.IMAGE_PNG_VALUE); } + + @Test + public void testPojoTransform() throws Exception + { + // Files + String sourceFileRef = UUID.randomUUID().toString(); + File sourceFile = getTestFile("quick." + sourceExtension, true); + String targetFileRef = UUID.randomUUID().toString(); + + // Transformation Request POJO + TransformRequest transformRequest = new TransformRequest(); + transformRequest.setRequestId("1"); + transformRequest.setSchema(1); + transformRequest.setClientData("Alfresco Digital Business Platform"); + transformRequest.setTransformRequestOptions(new HashMap<>()); + transformRequest.setSourceReference(sourceFileRef); + transformRequest.setSourceExtension(sourceExtension); + transformRequest.setSourceSize(sourceFile.length()); + transformRequest.setTargetExtension(targetExtension); + + // HTTP Request + HttpHeaders headers = new HttpHeaders(); + headers.set(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=quick." + sourceExtension); + ResponseEntity response = new ResponseEntity<>(new FileSystemResource( + sourceFile), headers, HttpStatus.OK); + + when(alfrescoSharedFileStoreClient.retrieveFile(sourceFileRef)).thenReturn(response); + when(alfrescoSharedFileStoreClient.saveFile(any())).thenReturn( + new FileRefResponse(new FileRefEntity(targetFileRef))); + when(mockExecutionResult.getExitValue()).thenReturn(0); + + // 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("/transform") + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_JSON_VALUE).content(tr)) + .andExpect( + status().is(HttpStatus.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()); + } } diff --git a/alfresco-docker-libreoffice/src/test/java/org/alfresco/transformer/LibreOfficeHttpRequestTest.java b/alfresco-docker-libreoffice/src/test/java/org/alfresco/transformer/LibreOfficeHttpRequestTest.java index cf392a06..f433a5bd 100644 --- a/alfresco-docker-libreoffice/src/test/java/org/alfresco/transformer/LibreOfficeHttpRequestTest.java +++ b/alfresco-docker-libreoffice/src/test/java/org/alfresco/transformer/LibreOfficeHttpRequestTest.java @@ -25,7 +25,6 @@ */ package org.alfresco.transformer; -import org.alfresco.transformer.AbstractHttpRequestTest; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @@ -47,5 +46,5 @@ public class LibreOfficeHttpRequestTest extends AbstractHttpRequestTest protected String getSourceExtension() { return "doc"; - }; + } } \ No newline at end of file diff --git a/alfresco-docker-tika/src/main/java/org/alfresco/transformer/Application.java b/alfresco-docker-tika/src/main/java/org/alfresco/transformer/Application.java index 778b51db..3fb6912f 100644 --- a/alfresco-docker-tika/src/main/java/org/alfresco/transformer/Application.java +++ b/alfresco-docker-tika/src/main/java/org/alfresco/transformer/Application.java @@ -11,15 +11,17 @@ */ package org.alfresco.transformer; -import io.micrometer.core.instrument.MeterRegistry; +import org.alfresco.transformer.executors.TikaJavaExecutor; 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.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.context.annotation.Bean; +import io.micrometer.core.instrument.MeterRegistry; + @SpringBootApplication @EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class}) public class Application @@ -27,10 +29,18 @@ public class Application @Value("${container.name}") private String containerName; - @Bean MeterRegistryCustomizer metricsCommonTags() { + @Bean + MeterRegistryCustomizer metricsCommonTags() + { return registry -> registry.config().commonTags("containerName", containerName); } + @Bean + public TikaJavaExecutor javaExecutor() throws Exception + { + return new TikaJavaExecutor(); + } + public static void main(String[] args) { SpringApplication.run(Application.class, args); diff --git a/alfresco-docker-tika/src/main/java/org/alfresco/transformer/TikaController.java b/alfresco-docker-tika/src/main/java/org/alfresco/transformer/TikaController.java index 65e6b6e1..e6614111 100644 --- a/alfresco-docker-tika/src/main/java/org/alfresco/transformer/TikaController.java +++ b/alfresco-docker-tika/src/main/java/org/alfresco/transformer/TikaController.java @@ -12,21 +12,31 @@ package org.alfresco.transformer; import static org.alfresco.repo.content.MimetypeMap.MIMETYPE_TEXT_PLAIN; -import static org.alfresco.transformer.Tika.INCLUDE_CONTENTS; -import static org.alfresco.transformer.Tika.NOT_EXTRACT_BOOKMARKS_TEXT; -import static org.alfresco.transformer.Tika.PDF_BOX; -import static org.alfresco.transformer.Tika.TARGET_ENCODING; -import static org.alfresco.transformer.Tika.TARGET_MIMETYPE; -import static org.alfresco.transformer.Tika.TRANSFORM_NAMES; +import static org.alfresco.transformer.executors.Tika.INCLUDE_CONTENTS; +import static org.alfresco.transformer.executors.Tika.NOT_EXTRACT_BOOKMARKS_TEXT; +import static org.alfresco.transformer.executors.Tika.PDF_BOX; +import static org.alfresco.transformer.executors.Tika.TARGET_ENCODING; +import static org.alfresco.transformer.executors.Tika.TARGET_MIMETYPE; +import static org.alfresco.transformer.executors.Tika.TRANSFORM_NAMES; +import static org.alfresco.transformer.fs.FileManager.createAttachment; +import static org.alfresco.transformer.fs.FileManager.createSourceFile; +import static org.alfresco.transformer.fs.FileManager.createTargetFile; +import static org.alfresco.transformer.fs.FileManager.createTargetFileName; +import static org.alfresco.transformer.logging.StandardMessages.ENTERPRISE_LICENCE; +import static org.alfresco.transformer.util.Util.stringToBoolean; import java.io.File; -import java.io.IOException; +import java.util.Arrays; import java.util.Map; import javax.servlet.http.HttpServletRequest; +import org.alfresco.transformer.exceptions.TransformException; +import org.alfresco.transformer.executors.TikaJavaExecutor; +import org.alfresco.transformer.logging.LogEntry; +import org.alfresco.transformer.probes.ProbeTestTransform; +import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.apache.tika.exception.TikaException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; import org.springframework.http.MediaType; @@ -35,95 +45,87 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; -import org.xml.sax.SAXException; /** * Controller for the Docker based Tika transformers. * * Status Codes: * - * 200 Success - * 400 Bad Request: Invalid target mimetype <mimetype> - * 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 + * 200 Success + * 400 Bad Request: Invalid target mimetype <mimetype> + * 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 */ @Controller public class TikaController extends AbstractTransformerController { - private Tika tika; + private static final Log logger = LogFactory.getLog(TikaController.class); @Autowired - public TikaController() throws TikaException, IOException, SAXException + private TikaJavaExecutor javaExecutor; + + @Autowired + public TikaController() { - logger = LogFactory.getLog(TikaController.class); logger.info("--------------------------------------------------------------------------------------------------------------------------------------------------------------"); - logEnterpriseLicenseMessage(); + Arrays.stream(ENTERPRISE_LICENCE.split("\\n")).forEach(logger::info); logger.info("Tika is from Apache. See the license at http://www.apache.org/licenses/LICENSE-2.0. or in /Apache\\ 2.0.txt"); logger.info("--------------------------------------------------------------------------------------------------------------------------------------------------------------"); - - tika = new Tika(); } @Override - protected String getTransformerName() + public String getTransformerName() { return "Tika"; } @Override - public void callTransform(String... args) - { - tika.transform(args); - } - - @Override - protected String version() + public String version() { return "Tika available"; } @Override - protected ProbeTestTransform getProbeTestTransform() + public ProbeTestTransform getProbeTestTransform() { // See the Javadoc on this method and Probes.md for the choice of these values. // the livenessPercentage is a little large as Tika does tend to suffer from slow transforms that class with a gc. - return new ProbeTestTransform(this, "quick.pdf", "quick.txt", - 60, 16, 400, 10240, 60*30+1, 60*15+20) + return new ProbeTestTransform(this, logger, "quick.pdf", "quick.txt", + 60, 16, 400, 10240, 60 * 30 + 1, 60 * 15 + 20) { @Override protected void executeTransformCommand(File sourceFile, File targetFile) { - TikaController.this.callTransform(sourceFile, targetFile, PDF_BOX, - TARGET_MIMETYPE+MIMETYPE_TEXT_PLAIN, TARGET_ENCODING+"UTF-8"); + javaExecutor.call(sourceFile, targetFile, PDF_BOX, + TARGET_MIMETYPE + MIMETYPE_TEXT_PLAIN, TARGET_ENCODING + "UTF-8"); } }; } @PostMapping(value = "/transform", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity transform(HttpServletRequest request, - @RequestParam("file") MultipartFile sourceMultipartFile, - @RequestParam("targetExtension") String targetExtension, - @RequestParam("targetMimetype") String targetMimetype, - @RequestParam("targetEncoding") String targetEncoding, + @RequestParam("file") MultipartFile sourceMultipartFile, + @RequestParam("targetExtension") String targetExtension, + @RequestParam("targetMimetype") String targetMimetype, + @RequestParam("targetEncoding") String targetEncoding, - @RequestParam(value = "timeout", required = false) Long timeout, - @RequestParam(value = "testDelay", required = false) Long testDelay, + @RequestParam(value = "timeout", required = false) Long timeout, + @RequestParam(value = "testDelay", required = false) Long testDelay, - @RequestParam(value = "transform") String transform, - @RequestParam(value="includeContents", required = false) Boolean includeContents, - @RequestParam(value="notExtractBookmarksText", required = false) Boolean notExtractBookmarksText) - + @RequestParam(value = "transform") String transform, + @RequestParam(value = "includeContents", required = false) Boolean includeContents, + @RequestParam(value = "notExtractBookmarksText", required = false) Boolean notExtractBookmarksText) { if (!TRANSFORM_NAMES.contains(transform)) { @@ -131,6 +133,7 @@ public class TikaController extends AbstractTransformerController } String targetFilename = createTargetFileName(sourceMultipartFile.getOriginalFilename(), targetExtension); + getProbeTestTransform().incrementTransformerCount(); File sourceFile = createSourceFile(request, sourceMultipartFile); File targetFile = createTargetFile(request, targetFilename); // Both files are deleted by TransformInterceptor.afterCompletion @@ -138,16 +141,21 @@ public class TikaController extends AbstractTransformerController // TODO Consider streaming the request and response rather than using temporary files // https://www.logicbig.com/tutorials/spring-framework/spring-web-mvc/streaming-response-body.html - callTransform(sourceFile, targetFile, transform, - includeContents != null && includeContents ? INCLUDE_CONTENTS : null, - notExtractBookmarksText != null && notExtractBookmarksText ? NOT_EXTRACT_BOOKMARKS_TEXT: null, - TARGET_MIMETYPE+targetMimetype, TARGET_ENCODING+targetEncoding); + javaExecutor.call(sourceFile, targetFile, transform, + includeContents != null && includeContents ? INCLUDE_CONTENTS : null, + notExtractBookmarksText != null && notExtractBookmarksText ? NOT_EXTRACT_BOOKMARKS_TEXT : null, + TARGET_MIMETYPE + targetMimetype, TARGET_ENCODING + targetEncoding); - return createAttachment(targetFilename, targetFile, testDelay); + final ResponseEntity body = createAttachment(targetFilename, targetFile); + LogEntry.setTargetSize(targetFile.length()); + long time = LogEntry.setStatusCodeAndMessage(200, "Success"); + time += LogEntry.addDelay(testDelay); + getProbeTestTransform().recordTransformTime(time); + return body; } @Override - protected void processTransform(File sourceFile, File targetFile, + public void processTransform(File sourceFile, File targetFile, Map transformOptions, Long timeout) { @@ -157,9 +165,9 @@ public class TikaController extends AbstractTransformerController String targetMimetype = transformOptions.get("targetMimetype"); String targetEncoding = transformOptions.get("targetEncoding"); - callTransform(sourceFile, targetFile, transform, + javaExecutor.call(sourceFile, targetFile, transform, includeContents != null && includeContents ? INCLUDE_CONTENTS : null, - notExtractBookmarksText != null && notExtractBookmarksText ? NOT_EXTRACT_BOOKMARKS_TEXT: null, + notExtractBookmarksText != null && notExtractBookmarksText ? NOT_EXTRACT_BOOKMARKS_TEXT : null, TARGET_MIMETYPE + targetMimetype, TARGET_ENCODING + targetEncoding); } } diff --git a/alfresco-docker-tika/src/main/java/org/alfresco/transformer/Tika.java b/alfresco-docker-tika/src/main/java/org/alfresco/transformer/executors/Tika.java similarity index 93% rename from alfresco-docker-tika/src/main/java/org/alfresco/transformer/Tika.java rename to alfresco-docker-tika/src/main/java/org/alfresco/transformer/executors/Tika.java index c97993cb..5e63670d 100644 --- a/alfresco-docker-tika/src/main/java/org/alfresco/transformer/Tika.java +++ b/alfresco-docker-tika/src/main/java/org/alfresco/transformer/executors/Tika.java @@ -9,7 +9,36 @@ * agreement is prohibited. * #L% */ -package org.alfresco.transformer; +package org.alfresco.transformer.executors; + +import static java.util.Arrays.asList; +import static org.alfresco.repo.content.MimetypeMap.MIMETYPE_HTML; +import static org.alfresco.repo.content.MimetypeMap.MIMETYPE_IMAGE_JPEG; +import static org.alfresco.repo.content.MimetypeMap.MIMETYPE_IMAGE_PNG; +import static org.alfresco.repo.content.MimetypeMap.MIMETYPE_IMAGE_TIFF; +import static org.alfresco.repo.content.MimetypeMap.MIMETYPE_TEXT_CSV; +import static org.alfresco.repo.content.MimetypeMap.MIMETYPE_TEXT_PLAIN; +import static org.alfresco.repo.content.MimetypeMap.MIMETYPE_XHTML; +import static org.alfresco.repo.content.MimetypeMap.MIMETYPE_XML; + +import java.io.BufferedInputStream; +import java.io.BufferedWriter; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.URL; +import java.util.List; +import java.util.regex.Pattern; + +import javax.xml.transform.OutputKeys; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.sax.SAXTransformerFactory; +import javax.xml.transform.sax.TransformerHandler; +import javax.xml.transform.stream.StreamResult; import org.apache.tika.config.TikaConfig; import org.apache.tika.exception.TikaException; @@ -30,19 +59,6 @@ import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; -import javax.xml.transform.OutputKeys; -import javax.xml.transform.TransformerConfigurationException; -import javax.xml.transform.sax.SAXTransformerFactory; -import javax.xml.transform.sax.TransformerHandler; -import javax.xml.transform.stream.StreamResult; -import java.io.*; -import java.net.URL; -import java.util.Arrays; -import java.util.List; -import java.util.regex.Pattern; - -import static org.alfresco.repo.content.MimetypeMap.*; - /** * Stripped down command line Tika transformers. Not actually run as a separate process, but the code fits the patten * used by transformers that do. @@ -424,7 +440,7 @@ public class Tika public static final String TIKA_AUTO = "TikaAuto"; public static final String TEXT_MINING = "TextMining"; - public static final List TRANSFORM_NAMES = Arrays.asList( + public static final List TRANSFORM_NAMES = asList( ARCHIVE, OUTLOOK_MSG, PDF_BOX, POI_OFFICE, POI, POI_OO_XML, TIKA_AUTO, TEXT_MINING); public static final String TARGET_MIMETYPE = "--targetMimetype="; @@ -445,17 +461,17 @@ public class Tika public static final String XML = "xml"; public static final String ZIP = "zip"; - private Parser packageParser = new PackageParser(); - private Parser pdfParser = new PDFParser(); - private Parser officeParser = new OfficeParser(); - private Parser autoDetectParser; - private Parser ooXmlParser = new OOXMLParser(); - private Parser tikaOfficeDetectParser = new TikaOfficeDetectParser(); - private PDFParserConfig pdfParserConfig = new PDFParserConfig(); + private final Parser packageParser = new PackageParser(); + private final Parser pdfParser = new PDFParser(); + private final Parser officeParser = new OfficeParser(); + private final Parser autoDetectParser; + private final Parser ooXmlParser = new OOXMLParser(); + private final Parser tikaOfficeDetectParser = new TikaOfficeDetectParser(); + private final PDFParserConfig pdfParserConfig = new PDFParserConfig(); private DocumentSelector pdfBoxEmbededDocumentSelector = new DocumentSelector() { - private List disabledMediaTypes = Arrays.asList(new String[] {MIMETYPE_IMAGE_JPEG, MIMETYPE_IMAGE_TIFF, MIMETYPE_IMAGE_PNG}); + private final List disabledMediaTypes = asList(MIMETYPE_IMAGE_JPEG, MIMETYPE_IMAGE_TIFF, MIMETYPE_IMAGE_PNG); @Override public boolean select(Metadata metadata) @@ -628,17 +644,14 @@ public class Tika String sourceFilename, String targetFilename, String targetMimetype, String targetEncoding) { - InputStream is = null; - OutputStream os = null; - Writer ow = null; - try + try (InputStream is = new BufferedInputStream(new FileInputStream(sourceFilename)); + OutputStream os = new FileOutputStream(targetFilename); + Writer ow = new BufferedWriter(new OutputStreamWriter(os, targetEncoding))) { - is = new BufferedInputStream(new FileInputStream(sourceFilename)); - os = new FileOutputStream(targetFilename); - ow = new BufferedWriter(new OutputStreamWriter(os, targetEncoding)); Metadata metadata = new Metadata(); - ParseContext context = buildParseContext(documentSelector, includeContents, notExtractBookmarksText); + ParseContext context = buildParseContext(documentSelector, includeContents, + notExtractBookmarksText); ContentHandler handler = getContentHandler(targetMimetype, ow); parser.parse(is, handler, metadata, context); @@ -647,24 +660,9 @@ public class Tika { throw new IllegalStateException(e.getMessage(), e); } - finally - { - if (is != null) - { - try { is.close(); } catch (Throwable e) {} - } - if (os != null) - { - try { os.close(); } catch (Throwable e) {} - } - if (ow != null) - { - try { ow.close(); } catch (Throwable e) {} - } - } } - protected ContentHandler getContentHandler(String targetMimetype, Writer output) + private ContentHandler getContentHandler(String targetMimetype, Writer output) { try { @@ -676,7 +674,7 @@ public class Tika else { SAXTransformerFactory factory = (SAXTransformerFactory)SAXTransformerFactory.newInstance(); - TransformerHandler transformerHandler = null; + TransformerHandler transformerHandler; transformerHandler = factory.newTransformerHandler(); transformerHandler.getTransformer().setOutputProperty(OutputKeys.INDENT, "yes"); transformerHandler.setResult(new StreamResult(output)); @@ -792,7 +790,8 @@ public class Tika } } - protected ParseContext buildParseContext(DocumentSelector documentSelector, Boolean includeContents, Boolean notExtractBookmarksText) + private ParseContext buildParseContext(DocumentSelector documentSelector, + Boolean includeContents, Boolean notExtractBookmarksText) { ParseContext context = new ParseContext(); diff --git a/alfresco-docker-tika/src/main/java/org/alfresco/transformer/executors/TikaJavaExecutor.java b/alfresco-docker-tika/src/main/java/org/alfresco/transformer/executors/TikaJavaExecutor.java new file mode 100644 index 00000000..84af7ac5 --- /dev/null +++ b/alfresco-docker-tika/src/main/java/org/alfresco/transformer/executors/TikaJavaExecutor.java @@ -0,0 +1,92 @@ +package org.alfresco.transformer.executors; + +import org.springframework.stereotype.Component; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.StringJoiner; + +import org.alfresco.transformer.exceptions.TransformException; +import org.alfresco.transformer.logging.LogEntry; +import org.apache.tika.exception.TikaException; +import org.springframework.beans.factory.annotation.Autowired; +import org.xml.sax.SAXException; + +@Component +public class TikaJavaExecutor implements JavaExecutor +{ + private final Tika tika; + + @Autowired + public TikaJavaExecutor() throws TikaException, IOException, SAXException + { + tika = new Tika(); + } + + @Override + public void call(File sourceFile, File targetFile, String... args) + throws TransformException + { + args = buildArgs(sourceFile, targetFile, args); + try + { + tika.transform(args); + } + catch (IllegalArgumentException e) + { + throw new TransformException(400, getMessage(e)); + } + catch (Exception e) + { + throw new TransformException(500, getMessage(e)); + } + if (!targetFile.exists() || targetFile.length() == 0) + { + throw new TransformException(500, "Transformer failed to create an output file"); + } + } + + private static String getMessage(Exception e) + { + return e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage(); + } + + private static String[] buildArgs(File sourceFile, File targetFile, String[] args) + { + ArrayList methodArgs = new ArrayList<>(args.length + 2); + StringJoiner sj = new StringJoiner(" "); + for (String arg : args) + { + addArg(methodArgs, sj, arg); + } + + addFileArg(methodArgs, sj, sourceFile); + addFileArg(methodArgs, sj, targetFile); + + LogEntry.setOptions(sj.toString()); + + return methodArgs.toArray(new String[0]); + } + + private static void addArg(ArrayList methodArgs, StringJoiner sj, String arg) + { + if (arg != null) + { + sj.add(arg); + methodArgs.add(arg); + } + } + + private static void addFileArg(ArrayList methodArgs, StringJoiner sj, File arg) + { + if (arg != null) + { + String path = arg.getAbsolutePath(); + int i = path.lastIndexOf('.'); + String ext = i == -1 ? "???" : path.substring(i + 1); + sj.add(ext); + methodArgs.add(path); + } + } +} diff --git a/alfresco-docker-tika/src/main/java/org/alfresco/transformer/TikaOfficeDetectParser.java b/alfresco-docker-tika/src/main/java/org/alfresco/transformer/executors/TikaOfficeDetectParser.java similarity index 93% rename from alfresco-docker-tika/src/main/java/org/alfresco/transformer/TikaOfficeDetectParser.java rename to alfresco-docker-tika/src/main/java/org/alfresco/transformer/executors/TikaOfficeDetectParser.java index d5607bbb..bcb0abe6 100644 --- a/alfresco-docker-tika/src/main/java/org/alfresco/transformer/TikaOfficeDetectParser.java +++ b/alfresco-docker-tika/src/main/java/org/alfresco/transformer/executors/TikaOfficeDetectParser.java @@ -23,7 +23,7 @@ * along with Alfresco. If not, see . * #L% */ -package org.alfresco.transformer; +package org.alfresco.transformer.executors; import java.io.IOException; import java.io.InputStream; @@ -43,7 +43,7 @@ import org.apache.tika.parser.microsoft.ooxml.OOXMLParser; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; -///////// THIS FILE IS A COPY OF THE CODE IN alfresco-repository ///////////// +///////// THIS FILE WAS A COPY OF THE CODE IN alfresco-repository ///////////// /** * Apache Tika assumes that @@ -58,11 +58,11 @@ import org.xml.sax.SAXException; * @author Nick Burch */ public class TikaOfficeDetectParser implements Parser { - private Parser ole2Parser = new OfficeParser(); - private Parser ooxmlParser = new OOXMLParser(); + private final Parser ole2Parser = new OfficeParser(); + private final Parser ooxmlParser = new OOXMLParser(); public Set getSupportedTypes(ParseContext parseContext) { - Set types = new HashSet(); + Set types = new HashSet<>(); types.addAll(ole2Parser.getSupportedTypes(parseContext)); types.addAll(ooxmlParser.getSupportedTypes(parseContext)); return types; diff --git a/alfresco-docker-tika/src/test/java/org/alfresco/transformer/TikaControllerTest.java b/alfresco-docker-tika/src/test/java/org/alfresco/transformer/TikaControllerTest.java index 412be7ea..a8acc050 100644 --- a/alfresco-docker-tika/src/test/java/org/alfresco/transformer/TikaControllerTest.java +++ b/alfresco-docker-tika/src/test/java/org/alfresco/transformer/TikaControllerTest.java @@ -37,41 +37,68 @@ import static org.alfresco.repo.content.MimetypeMap.MIMETYPE_WORD; import static org.alfresco.repo.content.MimetypeMap.MIMETYPE_XHTML; import static org.alfresco.repo.content.MimetypeMap.MIMETYPE_XML; import static org.alfresco.repo.content.MimetypeMap.MIMETYPE_ZIP; -import static org.alfresco.transformer.Tika.ARCHIVE; -import static org.alfresco.transformer.Tika.CSV; -import static org.alfresco.transformer.Tika.DOC; -import static org.alfresco.transformer.Tika.DOCX; -import static org.alfresco.transformer.Tika.HTML; -import static org.alfresco.transformer.Tika.MSG; -import static org.alfresco.transformer.Tika.OUTLOOK_MSG; -import static org.alfresco.transformer.Tika.PDF; -import static org.alfresco.transformer.Tika.PDF_BOX; -import static org.alfresco.transformer.Tika.POI; -import static org.alfresco.transformer.Tika.POI_OFFICE; -import static org.alfresco.transformer.Tika.POI_OO_XML; -import static org.alfresco.transformer.Tika.PPTX; -import static org.alfresco.transformer.Tika.TEXT_MINING; -import static org.alfresco.transformer.Tika.TIKA_AUTO; -import static org.alfresco.transformer.Tika.TXT; -import static org.alfresco.transformer.Tika.XHTML; -import static org.alfresco.transformer.Tika.XML; -import static org.alfresco.transformer.Tika.XSLX; -import static org.alfresco.transformer.Tika.ZIP; -import static org.springframework.test.util.AssertionErrors.assertTrue; +import static org.alfresco.transformer.executors.Tika.ARCHIVE; +import static org.alfresco.transformer.executors.Tika.CSV; +import static org.alfresco.transformer.executors.Tika.DOC; +import static org.alfresco.transformer.executors.Tika.DOCX; +import static org.alfresco.transformer.executors.Tika.HTML; +import static org.alfresco.transformer.executors.Tika.MSG; +import static org.alfresco.transformer.executors.Tika.OUTLOOK_MSG; +import static org.alfresco.transformer.executors.Tika.PDF; +import static org.alfresco.transformer.executors.Tika.PDF_BOX; +import static org.alfresco.transformer.executors.Tika.POI; +import static org.alfresco.transformer.executors.Tika.POI_OFFICE; +import static org.alfresco.transformer.executors.Tika.POI_OO_XML; +import static org.alfresco.transformer.executors.Tika.PPTX; +import static org.alfresco.transformer.executors.Tika.TEXT_MINING; +import static org.alfresco.transformer.executors.Tika.TIKA_AUTO; +import static org.alfresco.transformer.executors.Tika.TXT; +import static org.alfresco.transformer.executors.Tika.XHTML; +import static org.alfresco.transformer.executors.Tika.XML; +import static org.alfresco.transformer.executors.Tika.XSLX; +import static org.alfresco.transformer.executors.Tika.ZIP; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import org.alfresco.transform.client.model.TransformReply; import org.alfresco.transform.client.model.TransformRequest; +import org.alfresco.transformer.executors.TikaJavaExecutor; +import org.alfresco.transformer.model.FileRefEntity; +import org.alfresco.transformer.model.FileRefResponse; +import org.alfresco.util.exec.RuntimeExec; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.stubbing.Answer; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.util.StringUtils; /** * Test the TikaController without a server. @@ -81,37 +108,126 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilde @WebMvcTest(TikaController.class) public class TikaControllerTest extends AbstractTransformerControllerTest { - public static final String EXPECTED_XHTML_CONTENT_CONTAINS = "

The quick brown fox jumps over the lazy dog

"; - public static final String EXPECTED_TEXT_CONTENT_CONTAINS = "The quick brown fox jumps over the lazy dog"; - public static final String EXPECTED_MSG_CONTENT_CONTAINS = "Recipients\n" + - "\tmark.rogers@alfresco.com; speedy@quick.com; mrquick@nowhere.com\n" + - "\n" + - "The quick brown fox jumps over the lazy dogs"; - public static final String EXPECTED_CSV_CONTENT_CONTAINS = "\"The\",\"quick\",\"brown\",\"fox\""; + private static final String EXPECTED_XHTML_CONTENT_CONTAINS = "

The quick brown fox jumps over the lazy dog

"; + private static final String EXPECTED_TEXT_CONTENT_CONTAINS = "The quick brown fox jumps over the lazy dog"; + private static final String EXPECTED_MSG_CONTENT_CONTAINS = "Recipients\n" + + "\tmark.rogers@alfresco.com; speedy@quick.com; mrquick@nowhere.com\n" + + "\n" + + "The quick brown fox jumps over the lazy dogs"; + private static final String EXPECTED_CSV_CONTENT_CONTAINS = "\"The\",\"quick\",\"brown\",\"fox\""; + @Mock + private RuntimeExec.ExecutionResult mockExecutionResult; + + @Mock + private RuntimeExec mockTransformCommand; + + @Mock + private RuntimeExec mockCheckCommand; + + @SpyBean + private TikaJavaExecutor javaExecutor; + @SpyBean private TikaController controller; - String transform = PDF_BOX; - String targetEncoding = "UTF-8"; - String targetMimetype = MIMETYPE_TEXT_PLAIN; + private String transform = PDF_BOX; + private String targetEncoding = "UTF-8"; + private String targetMimetype = MIMETYPE_TEXT_PLAIN; @Before - public void before() throws Exception + public void before() { - controller.setAlfrescoSharedFileStoreClient(alfrescoSharedFileStoreClient); - super.controller = controller; - sourceExtension = "pdf"; targetExtension = "txt"; } + @Override + protected void mockTransformCommand(String sourceExtension, + String targetExtension, String sourceMimetype, + boolean readTargetFileBytes) throws IOException + { + this.sourceExtension = sourceExtension; + this.targetExtension = targetExtension; + this.sourceMimetype = sourceMimetype; + + expectedOptions = null; + expectedSourceSuffix = null; + expectedSourceFileBytes = readTestFile(sourceExtension); + expectedTargetFileBytes = readTargetFileBytes ? readTestFile(targetExtension) : null; + sourceFile = new MockMultipartFile("file", "quick." + sourceExtension, sourceMimetype, + expectedSourceFileBytes); + + when(mockTransformCommand.execute(any(), anyLong())).thenAnswer( + (Answer) invocation -> { + Map actualProperties = invocation.getArgument(0); + assertEquals("There should be 3 properties", 3, actualProperties.size()); + + String actualOptions = actualProperties.get("options"); + String actualSource = actualProperties.get("source"); + String actualTarget = actualProperties.get("target"); + String actualTargetExtension = StringUtils.getFilenameExtension(actualTarget); + + assertNotNull(actualSource); + assertNotNull(actualTarget); + if (expectedSourceSuffix != null) + { + assertTrue( + "The source file \"" + actualSource + "\" should have ended in \"" + expectedSourceSuffix + "\"", + actualSource.endsWith(expectedSourceSuffix)); + actualSource = actualSource.substring(0, + actualSource.length() - expectedSourceSuffix.length()); + } + + assertNotNull(actualOptions); + if (expectedOptions != null) + { + assertEquals("expectedOptions", expectedOptions, actualOptions); + } + + Long actualTimeout = invocation.getArgument(1); + assertNotNull(actualTimeout); + if (expectedTimeout != null) + { + assertEquals("expectedTimeout", expectedTimeout, actualTimeout); + } + + // Copy a test file into the target file location if it exists + int i = actualTarget.lastIndexOf('_'); + if (i >= 0) + { + String testFilename = actualTarget.substring(i + 1); + File testFile = getTestFile(testFilename, false); + File targetFile = new File(actualTarget); + generateTargetFileFromResourceFile(actualTargetExtension, testFile, + targetFile); + } + + // Check the supplied source file has not been changed. + byte[] actualSourceFileBytes = Files.readAllBytes(new File(actualSource).toPath()); + assertTrue("Source file is not the same", + Arrays.equals(expectedSourceFileBytes, actualSourceFileBytes)); + + return mockExecutionResult; + }); + + when(mockExecutionResult.getExitValue()).thenReturn(0); + when(mockExecutionResult.getStdErr()).thenReturn("STDERROR"); + when(mockExecutionResult.getStdOut()).thenReturn("STDOUT"); + } + + @Override + protected AbstractTransformerController getController() + { + return controller; + } + private void transform(String transform, String sourceExtension, String targetExtension, String sourceMimetype, String targetMimetype, Boolean includeContents, String expectedContentContains) throws Exception { // We don't use targetFileBytes as some of the transforms contain different date text based on the os being used. - super.mockTransformCommand(controller, sourceExtension, targetExtension, sourceMimetype, false); + mockTransformCommand(sourceExtension, targetExtension, sourceMimetype, false); this.transform = transform; this.targetMimetype = targetMimetype; @@ -141,7 +257,7 @@ public class TikaControllerTest extends AbstractTransformerControllerTest @Override public void simpleTransformTest() throws Exception { - super.mockTransformCommand(controller, PDF, TXT, MIMETYPE_PDF, true); + mockTransformCommand(PDF, TXT, MIMETYPE_PDF, true); super.simpleTransformTest(); } @@ -149,21 +265,13 @@ public class TikaControllerTest extends AbstractTransformerControllerTest @Override public void testDelayTest() throws Exception { - super.mockTransformCommand(controller, PDF, TXT, MIMETYPE_PDF, true); + mockTransformCommand(PDF, TXT, MIMETYPE_PDF, true); super.testDelayTest(); } @Test @Override - public void badExitCodeTest() throws Exception - { - // Ignore the test in super class as the Tika transforms are real rather than mocked up. - // It is the mock that returns a non zero exit code. - } - - @Test - @Override - public void noTargetFileTest() throws Exception + public void noTargetFileTest() { // Ignore the test in super class as the Tika transforms are real rather than mocked up. // It is the mock that returns a zero length file for other transformers, when we supply an invalid targetExtension. @@ -175,7 +283,7 @@ public class TikaControllerTest extends AbstractTransformerControllerTest @Override public void dotDotSourceFilenameTest() throws Exception { - super.mockTransformCommand(controller, PDF, TXT, MIMETYPE_PDF, true); + mockTransformCommand(PDF, TXT, MIMETYPE_PDF, true); super.dotDotSourceFilenameTest(); } @@ -183,7 +291,7 @@ public class TikaControllerTest extends AbstractTransformerControllerTest @Override public void noExtensionSourceFilenameTest() throws Exception { - super.mockTransformCommand(controller, PDF, TXT, MIMETYPE_PDF, true); + mockTransformCommand(PDF, TXT, MIMETYPE_PDF, true); super.noExtensionSourceFilenameTest(); } @@ -191,7 +299,7 @@ public class TikaControllerTest extends AbstractTransformerControllerTest @Override public void badSourceFilenameTest() throws Exception { - super.mockTransformCommand(controller, PDF, TXT, MIMETYPE_PDF, true); + mockTransformCommand(PDF, TXT, MIMETYPE_PDF, true); super.badSourceFilenameTest(); } @@ -199,7 +307,7 @@ public class TikaControllerTest extends AbstractTransformerControllerTest @Override public void blankSourceFilenameTest() throws Exception { - super.mockTransformCommand(controller, PDF, TXT, MIMETYPE_PDF, true); + mockTransformCommand(PDF, TXT, MIMETYPE_PDF, true); super.blankSourceFilenameTest(); } @@ -207,7 +315,7 @@ public class TikaControllerTest extends AbstractTransformerControllerTest @Override public void noTargetExtensionTest() throws Exception { - super.mockTransformCommand(controller, PDF, TXT, MIMETYPE_PDF, true); + mockTransformCommand(PDF, TXT, MIMETYPE_PDF, true); super.noTargetExtensionTest(); } @@ -215,7 +323,7 @@ public class TikaControllerTest extends AbstractTransformerControllerTest @Override public void calculateMaxTime() throws Exception { - super.mockTransformCommand(controller, PDF, TXT, MIMETYPE_PDF, true); + mockTransformCommand(PDF, TXT, MIMETYPE_PDF, true); super.calculateMaxTime(); } @@ -224,7 +332,7 @@ public class TikaControllerTest extends AbstractTransformerControllerTest @Test public void badEncodingTest() throws Exception { - super.mockTransformCommand(controller, PDF, TXT, MIMETYPE_PDF, true); + mockTransformCommand(PDF, TXT, MIMETYPE_PDF, true); targetEncoding = "rubbish"; mockMvc.perform(mockMvcRequest("/transform", sourceFile, "targetExtension", targetExtension)) .andExpect(status().is(500)); @@ -388,7 +496,7 @@ public class TikaControllerTest extends AbstractTransformerControllerTest @Test public void pdfToTxtExtractBookmarksTest() throws Exception { - super.mockTransformCommand(controller, PDF, TXT, MIMETYPE_PDF, true); + mockTransformCommand(PDF, TXT, MIMETYPE_PDF, true); mockMvc.perform(mockMvcRequest("/transform", sourceFile, "targetExtension", targetExtension).param("notExtractBookmarksText", "true")) .andExpect(status().is(200)) .andExpect(header().string("Content-Disposition", "attachment; filename*= UTF-8''quick." + targetExtension)); @@ -405,4 +513,54 @@ public class TikaControllerTest extends AbstractTransformerControllerTest transformRequest.getTransformRequestOptions().put("targetMimetype", MediaType.TEXT_PLAIN_VALUE); transformRequest.getTransformRequestOptions().put("targetEncoding", "UTF-8"); } + + @Test + public void testPojoTransform() throws Exception + { + // Files + String sourceFileRef = UUID.randomUUID().toString(); + File sourceFile = getTestFile("quick." + sourceExtension, true); + String targetFileRef = UUID.randomUUID().toString(); + + + // Transformation Request POJO + TransformRequest transformRequest = new TransformRequest(); + transformRequest.setRequestId("1"); + transformRequest.setSchema(1); + transformRequest.setClientData("Alfresco Digital Business Platform"); + transformRequest.setTransformRequestOptions(new HashMap<>()); + transformRequest.setSourceReference(sourceFileRef); + transformRequest.setSourceExtension(sourceExtension); + transformRequest.setSourceSize(sourceFile.length()); + transformRequest.setTargetExtension(targetExtension); + + // HTTP Request + HttpHeaders headers = new HttpHeaders(); + headers.set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=quick." + sourceExtension); + ResponseEntity response = new ResponseEntity<>(new FileSystemResource( + sourceFile), headers, HttpStatus.OK); + + when(alfrescoSharedFileStoreClient.retrieveFile(sourceFileRef)).thenReturn(response); + when(alfrescoSharedFileStoreClient.saveFile(any())).thenReturn(new FileRefResponse(new FileRefEntity(targetFileRef))); + when(mockExecutionResult.getExitValue()).thenReturn(0); + + // 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("/transform") + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).content(tr)) + .andExpect(status().is(HttpStatus.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()); + } + } diff --git a/alfresco-transformer-base/README.md b/alfresco-transformer-base/README.md index b0e052ce..ad538e07 100644 --- a/alfresco-transformer-base/README.md +++ b/alfresco-transformer-base/README.md @@ -94,7 +94,7 @@ public class AlfrescoPdfRendererController extends AbstractTransformerController String options = args.toString(); LogEntry.setOptions(options); - Map properties = new HashMap(5); + Map properties = new HashMap<>(); properties.put("options", options); properties.put("source", sourceFile.getAbsolutePath()); properties.put("target", targetFile.getAbsolutePath()); diff --git a/alfresco-transformer-base/src/main/java/org/alfresco/transformer/AbstractTransformerController.java b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/AbstractTransformerController.java index 7813f98d..e74a62d3 100644 --- a/alfresco-transformer-base/src/main/java/org/alfresco/transformer/AbstractTransformerController.java +++ b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/AbstractTransformerController.java @@ -25,55 +25,39 @@ */ package org.alfresco.transformer; +import static org.alfresco.transformer.fs.FileManager.buildFile; +import static org.alfresco.transformer.fs.FileManager.createTargetFileName; +import static org.alfresco.transformer.fs.FileManager.getFilenameFromContentDisposition; +import static org.alfresco.transformer.fs.FileManager.save; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.net.MalformedURLException; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.StringJoiner; import java.util.stream.Collectors; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import org.alfresco.transform.client.model.TransformReply; import org.alfresco.transform.client.model.TransformRequest; import org.alfresco.transform.client.model.TransformRequestValidator; +import org.alfresco.transformer.clients.AlfrescoSharedFileStoreClient; +import org.alfresco.transformer.exceptions.TransformException; +import org.alfresco.transformer.logging.LogEntry; import org.alfresco.transformer.model.FileRefResponse; import org.alfresco.util.TempFileProvider; -import org.alfresco.util.exec.RuntimeExec; import org.apache.commons.logging.Log; -import org.springframework.beans.TypeMismatchException; +import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; -import org.springframework.core.io.UrlResource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.ui.Model; import org.springframework.util.StringUtils; import org.springframework.validation.DirectFieldBindingResult; import org.springframework.validation.Errors; -import org.springframework.web.bind.MissingServletRequestParameterException; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.multipart.MultipartFile; -import org.springframework.web.util.UriUtils; /** *

Abstract Controller, provides structure and helper methods to sub-class transformer controllers.

@@ -103,11 +87,9 @@ import org.springframework.web.util.UriUtils; *

Provides methods to help super classes perform /transform requests. Also responses to /version, /ready and /live * requests.

*/ -public abstract class AbstractTransformerController +public abstract class AbstractTransformerController implements TransformController { - public static final String SOURCE_FILE = "sourceFile"; - public static final String TARGET_FILE = "targetFile"; - public static final String FILENAME = "filename="; + private static final Log logger = LogFactory.getLog(AbstractTransformerController.class); @Autowired private AlfrescoSharedFileStoreClient alfrescoSharedFileStoreClient; @@ -115,123 +97,90 @@ public abstract class AbstractTransformerController @Autowired private TransformRequestValidator transformRequestValidator; - protected static Log logger; - - protected RuntimeExec transformCommand; - private RuntimeExec checkCommand; - - private ProbeTestTransform probeTestTransform = null; - - public void setTransformCommand(RuntimeExec runtimeExec) - { - transformCommand = runtimeExec; - } - - public void setCheckCommand(RuntimeExec runtimeExec) - { - checkCommand = runtimeExec; - } - - protected void logEnterpriseLicenseMessage() - { - logger.info("This image is only intended to be used with the Alfresco Enterprise Content Repository which is covered by "); - logger.info("https://www.alfresco.com/legal/agreements and https://www.alfresco.com/terms-use"); - logger.info(""); - logger.info("License rights for this program may be obtained from Alfresco Software, Ltd. pursuant to a written agreement"); - logger.info("and any use of this program without such an agreement is prohibited."); - logger.info(""); - } - - protected abstract String getTransformerName(); - /** * '/transform' endpoint which consumes and produces 'application/json' * * This is the way to tell Spring to redirect the request to this endpoint * instead of the older one, which produces 'html' * - * @param transformRequest The transformation request + * @param request The transformation request * @param timeout Transformation timeout * @return A transformation reply */ @PostMapping(value = "/transform", produces = APPLICATION_JSON_VALUE) @ResponseBody - public ResponseEntity transform(@RequestBody TransformRequest transformRequest, + public ResponseEntity transform(@RequestBody TransformRequest request, @RequestParam(value = "timeout", required = false) Long timeout) { - TransformReply transformReply = new TransformReply(); - transformReply.setRequestId(transformRequest.getRequestId()); - transformReply.setSourceReference(transformRequest.getSourceReference()); - transformReply.setSchema(transformRequest.getSchema()); - transformReply.setClientData(transformRequest.getClientData()); + final TransformReply reply = new TransformReply(); + reply.setRequestId(request.getRequestId()); + reply.setSourceReference(request.getSourceReference()); + reply.setSchema(request.getSchema()); + reply.setClientData(request.getClientData()); - Errors errors = validateTransformRequest(transformRequest); + final Errors errors = validateTransformRequest(request); if (!errors.getAllErrors().isEmpty()) { - transformReply.setStatus(HttpStatus.BAD_REQUEST.value()); - transformReply.setErrorDetails(errors.getAllErrors().stream().map(Object::toString) + reply.setStatus(HttpStatus.BAD_REQUEST.value()); + reply.setErrorDetails(errors.getAllErrors().stream().map(Object::toString) .collect(Collectors.joining(", "))); - return new ResponseEntity<>(transformReply, - HttpStatus.valueOf(transformReply.getStatus())); + return new ResponseEntity<>(reply, + HttpStatus.valueOf(reply.getStatus())); } // Load the source file File sourceFile; try { - sourceFile = loadSourceFile(transformRequest.getSourceReference()); + sourceFile = loadSourceFile(request.getSourceReference()); } catch (TransformException te) { - transformReply.setStatus(te.getStatusCode()); - transformReply - .setErrorDetails("Failed at reading the source file. " + te.getMessage()); + reply.setStatus(te.getStatusCode()); + reply .setErrorDetails("Failed at reading the source file. " + te.getMessage()); - return new ResponseEntity<>(transformReply, HttpStatus.valueOf(transformReply.getStatus())); + return new ResponseEntity<>(reply, HttpStatus.valueOf(reply.getStatus())); } catch (HttpClientErrorException hcee) { - transformReply.setStatus(hcee.getStatusCode().value()); - transformReply - .setErrorDetails("Failed at reading the source file. " + hcee.getMessage()); + reply.setStatus(hcee.getStatusCode().value()); + reply .setErrorDetails("Failed at reading the source file. " + hcee.getMessage()); - return new ResponseEntity<>(transformReply, HttpStatus.valueOf(transformReply.getStatus())); + return new ResponseEntity<>(reply, HttpStatus.valueOf(reply.getStatus())); } catch (Exception e) { - transformReply.setStatus(500); - transformReply.setErrorDetails("Failed at reading the source file. " + e.getMessage()); + reply.setStatus(500); + reply.setErrorDetails("Failed at reading the source file. " + e.getMessage()); - return new ResponseEntity<>(transformReply, HttpStatus.valueOf(transformReply.getStatus())); + return new ResponseEntity<>(reply, HttpStatus.valueOf(reply.getStatus())); } // Create local temp target file in order to run the transformation String targetFilename = createTargetFileName(sourceFile.getName(), - transformRequest.getTargetExtension()); + request.getTargetExtension()); File targetFile = buildFile(targetFilename); // Run the transformation try { processTransform(sourceFile, targetFile, - transformRequest.getTransformRequestOptions(), timeout); + request.getTransformRequestOptions(), timeout); } catch (TransformException te) { - transformReply.setStatus(te.getStatusCode()); - transformReply - .setErrorDetails("Failed at processing transformation. " + te.getMessage()); + reply.setStatus(te.getStatusCode()); + reply.setErrorDetails("Failed at processing transformation. " + te.getMessage()); - return new ResponseEntity<>(transformReply, HttpStatus.valueOf(transformReply.getStatus())); + return new ResponseEntity<>(reply, HttpStatus.valueOf(reply.getStatus())); } catch (Exception e) { - transformReply.setStatus(500); - transformReply - .setErrorDetails("Failed at processing transformation. " + e.getMessage()); + reply.setStatus(500); + reply.setErrorDetails("Failed at processing transformation. " + e.getMessage()); - return new ResponseEntity<>(transformReply, HttpStatus.valueOf(transformReply.getStatus())); + return new ResponseEntity<>(reply, HttpStatus.valueOf(reply.getStatus())); } // Write the target file @@ -242,211 +191,50 @@ public abstract class AbstractTransformerController } catch (TransformException te) { - transformReply.setStatus(te.getStatusCode()); - transformReply - .setErrorDetails("Failed at writing the transformed file. " + te.getMessage()); + reply.setStatus(te.getStatusCode()); + reply.setErrorDetails("Failed at writing the transformed file. " + te.getMessage()); - return new ResponseEntity<>(transformReply, HttpStatus.valueOf(transformReply.getStatus())); + return new ResponseEntity<>(reply, HttpStatus.valueOf(reply.getStatus())); } catch (HttpClientErrorException hcee) { - transformReply.setStatus(hcee.getStatusCode().value()); - transformReply - .setErrorDetails("Failed at writing the transformed file. " + hcee.getMessage()); + reply.setStatus(hcee.getStatusCode().value()); + reply.setErrorDetails("Failed at writing the transformed file. " + hcee.getMessage()); - return new ResponseEntity<>(transformReply, HttpStatus.valueOf(transformReply.getStatus())); + return new ResponseEntity<>(reply, HttpStatus.valueOf(reply.getStatus())); } catch (Exception e) { - transformReply.setStatus(500); - transformReply - .setErrorDetails("Failed at writing the transformed file. " + e.getMessage()); + reply.setStatus(500); + reply.setErrorDetails("Failed at writing the transformed file. " + e.getMessage()); - return new ResponseEntity<>(transformReply, HttpStatus.valueOf(transformReply.getStatus())); + return new ResponseEntity<>(reply, HttpStatus.valueOf(reply.getStatus())); } - transformReply.setTargetReference(targetRef.getEntry().getFileRef()); - transformReply.setStatus(HttpStatus.CREATED.value()); + reply.setTargetReference(targetRef.getEntry().getFileRef()); + reply.setStatus(HttpStatus.CREATED.value()); - return new ResponseEntity<>(transformReply, HttpStatus.valueOf(transformReply.getStatus())); + return new ResponseEntity<>(reply, HttpStatus.valueOf(reply.getStatus())); } - private Errors validateTransformRequest(TransformRequest transformRequest) + private Errors validateTransformRequest(final TransformRequest transformRequest) { DirectFieldBindingResult errors = new DirectFieldBindingResult(transformRequest, "request"); transformRequestValidator.validate(transformRequest, errors); return errors; } - protected abstract void processTransform(File sourceFile, File targetFile, - Map transformOptions, Long timeout); - - @RequestMapping("/version") - @ResponseBody - protected 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("/ready") - @ResponseBody - public String ready(HttpServletRequest request) - { - return probe(request, false); - } - - @GetMapping("/live") - @ResponseBody - public String live(HttpServletRequest request) - { - return probe(request, true); - } - - private String probe(HttpServletRequest request, boolean isLiveProbe) - { - return getProbeTestTransformInternal().doTransformOrNothing(request, isLiveProbe); - } - - private ProbeTestTransform getProbeTestTransformInternal() - { - if (probeTestTransform == null) - { - probeTestTransform = getProbeTestTransform(); - } - return probeTestTransform; - } - - abstract ProbeTestTransform getProbeTestTransform(); - - @GetMapping("/") - public String transformForm(Model model) - { - return "transformForm"; // the name of the template - } - - @GetMapping("/log") - public String log(Model model) - { - model.addAttribute("title", getTransformerName() + " Log Entries"); - Collection log = LogEntry.getLog(); - if (!log.isEmpty()) - { - model.addAttribute("log", log); - } - return "log"; // the name of the template - } - - @GetMapping("/error") - public String error() - { - return "error"; // the name of the template - } - - @ExceptionHandler(TypeMismatchException.class) - public void handleParamsTypeMismatch(HttpServletResponse response, MissingServletRequestParameterException e) throws IOException - { - String transformerName = getTransformerName(); - 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, transformerName+" - "+message); - } - - @ExceptionHandler(MissingServletRequestParameterException.class) - public void handleMissingParams(HttpServletResponse response, MissingServletRequestParameterException e) throws IOException - { - String transformerName = getTransformerName(); - 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, transformerName+" - "+message); - } - - @ExceptionHandler(TransformException.class) - public void transformExceptionWithMessage(HttpServletResponse response, TransformException e) throws IOException - { - String transformerName = getTransformerName(); - String message = e.getMessage(); - int statusCode = e.getStatusCode(); - - if (logger != null && logger.isErrorEnabled()) - { - logger.error(message); - } - - long time = LogEntry.setStatusCodeAndMessage(statusCode, message); - getProbeTestTransformInternal().recordTransformTime(time); - - // Forced to include the transformer name in the message (see commented out version of this method) - response.sendError(statusCode, transformerName+" - "+message); - } - - // Results in HTML rather than json but there is an error in the log about "template might not exist or might - // not be accessible by any of the configured Template Resolvers" for the transformer.html (which is correct - // because that failed). Looks like Spring only supports returning json or XML when returning an Object or even - // a ResponseEntity without this logged exception, which is a shame as it would have been nicer to have just - // added the transformerName to the Object. -// @ExceptionHandler(TransformException.class) -// public final Map transformExceptionWithMessage(HttpServletResponse response, TransformException e, WebRequest request) -// { -// String transformerName = getTransformerName(); -// String message = e.getMessage(); -// int statusCode = e.getStatusCode(); -// -// LogEntry.setStatusCodeAndMessage(statusCode, message); -// -// Map errorAttributes = new HashMap<>(); -// errorAttributes.put("title", transformerName); -// errorAttributes.put("message", message); -// errorAttributes.put("status", Integer.toString(statusCode)); -// errorAttributes.put("error", HttpStatus.valueOf(statusCode).getReasonPhrase()); -// return errorAttributes; -// } - /** * Loads the file with the specified sourceReference from Alfresco Shared File Store * * @param sourceReference reference to the file in Alfresco Shared File Store * @return the file containing the source content for the transformation */ - protected File loadSourceFile(String sourceReference) + private File loadSourceFile(final String sourceReference) { - ResponseEntity responseEntity = alfrescoSharedFileStoreClient .retrieveFile(sourceReference); - getProbeTestTransformInternal().incrementTransformerCount(); + getProbeTestTransform().incrementTransformerCount(); HttpHeaders headers = responseEntity.getHeaders(); String filename = getFilenameFromContentDisposition(headers); @@ -468,299 +256,4 @@ public abstract class AbstractTransformerController LogEntry.setSource(filename, size); return file; } - - - private String getFilenameFromContentDisposition(HttpHeaders headers) - { - String filename = ""; - String contentDisposition = headers.getFirst(HttpHeaders.CONTENT_DISPOSITION); - if (contentDisposition != null) - { - String[] strings = contentDisposition.split("; *"); - for (String string: strings) - { - if (string.startsWith(FILENAME)) - { - filename = string.substring(FILENAME.length()); - break; - } - } - } - return filename; - } - - /** - * Returns the file name for the target file - * - * @param fileName Desired file name - * @param targetExtension File extension - * @return Target file name - */ - protected String createTargetFileName(String fileName, String targetExtension) - { - String targetFilename = null; - String sourceFilename = fileName; - sourceFilename = StringUtils.getFilename(sourceFilename); - if (sourceFilename != null && !sourceFilename.isEmpty()) - { - String ext = StringUtils.getFilenameExtension(sourceFilename); - targetFilename = (ext != null && !ext.isEmpty() - ? sourceFilename.substring(0, sourceFilename.length()-ext.length()-1) - : sourceFilename)+ - '.'+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) - { - getProbeTestTransformInternal().incrementTransformerCount(); - 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) - { - File file = buildFile(filename); - request.setAttribute(TARGET_FILE, file); - return file; - } - - private File buildFile(String filename) - { - filename = checkFilename( false, filename); - LogEntry.setTarget(filename); - return TempFileProvider.createTempFile("target_", "_" + filename); - } - - /** - * 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 void save(Resource body, File file) - { - try - { - InputStream inputStream = body == null ? null : body.getInputStream(); - Files.copy(inputStream, 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); - } - } - - public void callTransform(File sourceFile, File targetFile, String... args) throws TransformException - { - args = buildArgs(sourceFile, targetFile, args); - try - { - callTransform(args); - } - catch (IllegalArgumentException e) - { - throw new TransformException(400, getMessage(e)); - } - catch (Exception e) - { - throw new TransformException(500, getMessage(e)); - } - if (!targetFile.exists() || targetFile.length() == 0) - { - throw new TransformException(500, "Transformer failed to create an output file"); - } - } - - private String getMessage(Exception e) - { - return e.getMessage() == null ? e.getClass().getSimpleName(): e.getMessage(); - } - - protected void callTransform(String[] args) - { - // Overridden when the transform is done in the JVM rather than in an external command. - } - - protected String[] buildArgs(File sourceFile, File targetFile, String[] args) - { - ArrayList methodArgs = new ArrayList<>(args.length+2); - StringJoiner sj = new StringJoiner(" "); - for (String arg: args) - { - addArg(methodArgs, sj, arg); - } - - addFileArg(methodArgs, sj, sourceFile); - addFileArg(methodArgs, sj, targetFile); - - LogEntry.setOptions(sj.toString()); - - return methodArgs.toArray(new String[methodArgs.size()]); - } - - private void addArg(ArrayList methodArgs, StringJoiner sj, String arg) - { - if (arg != null) - { - sj.add(arg); - methodArgs.add(arg); - } - } - - private void addFileArg(ArrayList methodArgs, StringJoiner sj, File arg) - { - if (arg != null) - { - String path = arg.getAbsolutePath(); - int i = path.lastIndexOf('.'); - String ext = i == -1 ? "???" : path.substring(i+1); - sj.add(ext); - methodArgs.add(path); - } - } - - protected void executeTransformCommand(String options, File sourceFile, File targetFile, Long timeout) - { - LogEntry.setOptions(options); - - Map properties = new HashMap(5); - properties.put("options", options); - properties.put("source", sourceFile.getAbsolutePath()); - properties.put("target", targetFile.getAbsolutePath()); - - executeTransformCommand(properties, targetFile, timeout); - } - - public void executeTransformCommand(Map properties, File targetFile, Long timeout) - { - timeout = timeout != null && timeout > 0 ? timeout : 0; - RuntimeExec.ExecutionResult result = transformCommand.execute(properties, timeout); - - if (result.getExitValue() != 0 && result.getStdErr() != null && result.getStdErr().length() > 0) - { - throw new TransformException(400, "Transformer exit code was not 0: \n" + result.getStdErr()); - } - - if (!targetFile.exists() || targetFile.length() == 0) - { - throw new TransformException(500, "Transformer failed to create an output file"); - } - } - - protected ResponseEntity createAttachment(String targetFilename, File targetFile, Long testDelay) - { - Resource targetResource = load(targetFile); - targetFilename = UriUtils.encodePath(StringUtils.getFilename(targetFilename), "UTF-8"); - ResponseEntity body = ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION, - "attachment; filename*= UTF-8''" + targetFilename).body(targetResource); - LogEntry.setTargetSize(targetFile.length()); - long time = LogEntry.setStatusCodeAndMessage(200, "Success"); - time += LogEntry.addDelay(testDelay); - getProbeTestTransformInternal().recordTransformTime(time); - return body; - } - - /** - * 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} - */ - protected Integer stringToInteger(String param) - { - return param == null ? null : Integer.parseInt(param); - } - - /** - * 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 Boolean} - */ - protected Boolean stringToBoolean(String param) - { - return param == null? null : Boolean.parseBoolean(param); - } - - public AlfrescoSharedFileStoreClient getAlfrescoSharedFileStoreClient() - { - return alfrescoSharedFileStoreClient; - } - - public void setAlfrescoSharedFileStoreClient( - AlfrescoSharedFileStoreClient alfrescoSharedFileStoreClient) - { - this.alfrescoSharedFileStoreClient = alfrescoSharedFileStoreClient; - } } diff --git a/alfresco-transformer-base/src/main/java/org/alfresco/transformer/Application.java b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/Application.java deleted file mode 100644 index bb2b9b7d..00000000 --- a/alfresco-transformer-base/src/main/java/org/alfresco/transformer/Application.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * #%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 . - * #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); - } - -} diff --git a/alfresco-transformer-base/src/main/java/org/alfresco/transformer/TransformController.java b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/TransformController.java new file mode 100644 index 00000000..e92d1bc5 --- /dev/null +++ b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/TransformController.java @@ -0,0 +1,137 @@ +package org.alfresco.transformer; + +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.transform.client.model.TransformReply; +import org.alfresco.transform.client.model.TransformRequest; +import org.alfresco.transformer.exceptions.TransformException; +import org.alfresco.transformer.logging.LogEntry; +import org.alfresco.transformer.probes.ProbeTestTransform; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.TypeMismatchException; +import org.springframework.http.ResponseEntity; +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.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +public interface TransformController +{ + Log logger = LogFactory.getLog(TransformController.class); + + ResponseEntity transform(TransformRequest transformRequest, Long timeout); + + void processTransform(File sourceFile, File targetFile, Map transformOptions, + Long timeout); + + String getTransformerName(); + + ProbeTestTransform getProbeTestTransform(); + + default String probe(HttpServletRequest request, boolean isLiveProbe) + { + return getProbeTestTransform().doTransformOrNothing(request, isLiveProbe); + } + + @RequestMapping("/version") + @ResponseBody + String version(); + + @GetMapping("/") + default String transformForm(Model model) + { + return "transformForm"; // the name of the template + } + + @GetMapping("/error") + default String error() + { + return "error"; // the name of the template + } + + @GetMapping("/log") + default String log(Model model) + { + model.addAttribute("title", getTransformerName() + " Log Entries"); + Collection log = LogEntry.getLog(); + if (!log.isEmpty()) + { + model.addAttribute("log", log); + } + return "log"; // the name of the template + } + + @GetMapping("/ready") + @ResponseBody + default String ready(HttpServletRequest request) + { + return probe(request, false); + } + + @GetMapping("/live") + @ResponseBody + default String live(HttpServletRequest request) + { + return probe(request, true); + } + + //region [Exception Handlers] + @ExceptionHandler(TypeMismatchException.class) + default void handleParamsTypeMismatch(HttpServletResponse response, + MissingServletRequestParameterException e) throws IOException + { + String transformerName = getTransformerName(); + String name = e.getParameterName(); + String message = "Request parameter " + name + " is of the wrong type"; + int statusCode = 400; + + logger.error(message); + + LogEntry.setStatusCodeAndMessage(statusCode, message); + + response.sendError(statusCode, transformerName + " - " + message); + } + + @ExceptionHandler(MissingServletRequestParameterException.class) + default void handleMissingParams(HttpServletResponse response, + MissingServletRequestParameterException e) throws IOException + { + String transformerName = getTransformerName(); + String name = e.getParameterName(); + String message = "Request parameter " + name + " is missing"; + int statusCode = 400; + + logger.error(message); + + LogEntry.setStatusCodeAndMessage(statusCode, message); + + response.sendError(statusCode, transformerName + " - " + message); + } + + @ExceptionHandler(TransformException.class) + default void transformExceptionWithMessage(HttpServletResponse response, + TransformException e) throws IOException + { + String transformerName = getTransformerName(); + String message = e.getMessage(); + int statusCode = e.getStatusCode(); + + logger.error(message); + + long time = LogEntry.setStatusCodeAndMessage(statusCode, message); + getProbeTestTransform().recordTransformTime(time); + + // Forced to include the transformer name in the message (see commented out version of this method) + response.sendError(statusCode, transformerName + " - " + message); + } + //endregion +} diff --git a/alfresco-transformer-base/src/main/java/org/alfresco/transformer/TransformInterceptor.java b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/TransformInterceptor.java index 90e7779f..be98a00a 100644 --- a/alfresco-transformer-base/src/main/java/org/alfresco/transformer/TransformInterceptor.java +++ b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/TransformInterceptor.java @@ -25,18 +25,22 @@ */ package org.alfresco.transformer; -import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; +import static org.alfresco.transformer.fs.FileManager.SOURCE_FILE; +import static org.alfresco.transformer.fs.FileManager.TARGET_FILE; +import static org.alfresco.transformer.fs.FileManager.deleteFile; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.io.File; + +import org.alfresco.transformer.logging.LogEntry; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; public class TransformInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, - HttpServletResponse response, Object handler) throws Exception + HttpServletResponse response, Object handler) { LogEntry.start(); return true; @@ -45,21 +49,11 @@ public class TransformInterceptor extends HandlerInterceptorAdapter @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, AbstractTransformerController.SOURCE_FILE); - deleteFile(request, AbstractTransformerController.TARGET_FILE); + deleteFile(request, SOURCE_FILE); + deleteFile(request, TARGET_FILE); LogEntry.complete(); } - - private void deleteFile(HttpServletRequest request, String attributeName) - { - File file = (File) request.getAttribute(attributeName); - if (file != null) - { - file.delete(); - } - } } diff --git a/alfresco-transformer-base/src/main/java/org/alfresco/transformer/AlfrescoSharedFileStoreClient.java b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/clients/AlfrescoSharedFileStoreClient.java similarity index 96% rename from alfresco-transformer-base/src/main/java/org/alfresco/transformer/AlfrescoSharedFileStoreClient.java rename to alfresco-transformer-base/src/main/java/org/alfresco/transformer/clients/AlfrescoSharedFileStoreClient.java index bbf8a033..4339ca5f 100644 --- a/alfresco-transformer-base/src/main/java/org/alfresco/transformer/AlfrescoSharedFileStoreClient.java +++ b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/clients/AlfrescoSharedFileStoreClient.java @@ -5,10 +5,11 @@ * pursuant to a written agreement and any use of this program without such an * agreement is prohibited. */ -package org.alfresco.transformer; +package org.alfresco.transformer.clients; import java.io.File; +import org.alfresco.transformer.exceptions.TransformException; import org.alfresco.transformer.model.FileRefResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; diff --git a/alfresco-transformer-base/src/main/java/org/alfresco/transformer/WebApplicationConfig.java b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/config/WebApplicationConfig.java similarity index 92% rename from alfresco-transformer-base/src/main/java/org/alfresco/transformer/WebApplicationConfig.java rename to alfresco-transformer-base/src/main/java/org/alfresco/transformer/config/WebApplicationConfig.java index df599f28..bac895ba 100644 --- a/alfresco-transformer-base/src/main/java/org/alfresco/transformer/WebApplicationConfig.java +++ b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/config/WebApplicationConfig.java @@ -23,8 +23,10 @@ * along with Alfresco. If not, see . * #L% */ -package org.alfresco.transformer; +package org.alfresco.transformer.config; +import org.alfresco.transformer.TransformInterceptor; +import org.alfresco.transformer.clients.AlfrescoSharedFileStoreClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; diff --git a/alfresco-transformer-base/src/main/java/org/alfresco/transformer/TransformException.java b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/exceptions/TransformException.java similarity index 94% rename from alfresco-transformer-base/src/main/java/org/alfresco/transformer/TransformException.java rename to alfresco-transformer-base/src/main/java/org/alfresco/transformer/exceptions/TransformException.java index 196c10d6..c7951c31 100644 --- a/alfresco-transformer-base/src/main/java/org/alfresco/transformer/TransformException.java +++ b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/exceptions/TransformException.java @@ -23,11 +23,11 @@ * along with Alfresco. If not, see . * #L% */ -package org.alfresco.transformer; +package org.alfresco.transformer.exceptions; public class TransformException extends RuntimeException { - private int statusCode; + private final int statusCode; public TransformException(int statusCode, String message) { diff --git a/alfresco-transformer-base/src/main/java/org/alfresco/transformer/executors/AbstractCommandExecutor.java b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/executors/AbstractCommandExecutor.java new file mode 100644 index 00000000..31a48263 --- /dev/null +++ b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/executors/AbstractCommandExecutor.java @@ -0,0 +1,67 @@ +package org.alfresco.transformer.executors; + +import java.io.File; +import java.util.Map; + +import org.alfresco.transformer.exceptions.TransformException; +import org.alfresco.util.exec.RuntimeExec; + +/** + */ +public abstract class AbstractCommandExecutor implements CommandExecutor +{ + private RuntimeExec transformCommand = createTransformCommand(); + private RuntimeExec checkCommand = createCheckCommand(); + + protected abstract RuntimeExec createTransformCommand(); + + protected abstract RuntimeExec createCheckCommand(); + + // todo remove these setters and and make the fields final + public void setTransformCommand(RuntimeExec re) { + transformCommand = re; + } + + public void setCheckCommand(RuntimeExec re) { + checkCommand = re; + } + + @Override + public void run(Map properties, File targetFile, Long timeout) + { + timeout = timeout != null && timeout > 0 ? timeout : 0; + RuntimeExec.ExecutionResult result = transformCommand.execute(properties, timeout); + + if (result.getExitValue() != 0 && result.getStdErr() != null && result.getStdErr().length() > 0) + { + throw new TransformException(400, "Transformer exit code was not 0: \n" + result.getStdErr()); + } + + if (!targetFile.exists() || targetFile.length() == 0) + { + throw new TransformException(500, "Transformer failed to create an output file"); + } + } + + @Override + public 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; + } +} diff --git a/alfresco-transformer-base/src/main/java/org/alfresco/transformer/executors/CommandExecutor.java b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/executors/CommandExecutor.java new file mode 100644 index 00000000..95ba382b --- /dev/null +++ b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/executors/CommandExecutor.java @@ -0,0 +1,45 @@ +package org.alfresco.transformer.executors; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.transformer.logging.LogEntry; + +/** + * Basic interface for executing transformations via Shell commands + * + * @author Cezar Leahu + */ +public interface CommandExecutor +{ + void run(Map properties, File targetFile, Long timeout); + + String version(); + + default void run(String options, File sourceFile, File targetFile, + Long timeout) + { + LogEntry.setOptions(options); + + Map properties = new HashMap<>(); + properties.put("options", options); + properties.put("source", sourceFile.getAbsolutePath()); + properties.put("target", targetFile.getAbsolutePath()); + + run(properties, targetFile, timeout); + } + + default void run(String options, File sourceFile, String pageRange, File + targetFile, Long timeout) + { + LogEntry.setOptions(pageRange + (pageRange.isEmpty() ? "" : " ") + options); + + Map properties = new HashMap<>(); + properties.put("options", options); + properties.put("source", sourceFile.getAbsolutePath() + pageRange); + properties.put("target", targetFile.getAbsolutePath()); + + run(properties, targetFile, timeout); + } +} diff --git a/alfresco-transformer-base/src/main/java/org/alfresco/transformer/executors/JavaExecutor.java b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/executors/JavaExecutor.java new file mode 100644 index 00000000..6887beb1 --- /dev/null +++ b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/executors/JavaExecutor.java @@ -0,0 +1,15 @@ +package org.alfresco.transformer.executors; + +import java.io.File; + +import org.alfresco.transformer.exceptions.TransformException; + +/** + * Basic interface for executing transformations inside Java/JVM + * + * @author Cezar Leahu + */ +public interface JavaExecutor +{ + void call(File sourceFile, File targetFile, String... args) throws TransformException; +} diff --git a/alfresco-transformer-base/src/main/java/org/alfresco/transformer/fs/FileManager.java b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/fs/FileManager.java new file mode 100644 index 00000000..7a1f61b5 --- /dev/null +++ b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/fs/FileManager.java @@ -0,0 +1,195 @@ +package org.alfresco.transformer.fs; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.Arrays; + +import javax.servlet.http.HttpServletRequest; + +import org.alfresco.transformer.exceptions.TransformException; +import org.alfresco.transformer.logging.LogEntry; +import org.alfresco.util.TempFileProvider; +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.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.util.UriUtils; + +/** + */ +public class FileManager +{ + public static final String SOURCE_FILE = "sourceFile"; + public static final String TARGET_FILE = "targetFile"; + private static final String FILENAME = "filename="; + + /** + * 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. + */ + public static File createTargetFile(HttpServletRequest request, String filename) + { + File file = buildFile(filename); + request.setAttribute(TARGET_FILE, file); + return file; + } + + public static File buildFile(String filename) + { + filename = checkFilename( false, filename); + LogEntry.setTarget(filename); + return TempFileProvider.createTempFile("target_", "_" + filename); + } + + /** + * 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 static 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 static 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); + } + } + + public static void save(Resource body, File file) + { + try + { + Files.copy(body.getInputStream(), file.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + catch (IOException e) + { + throw new TransformException(507, "Failed to store the source file", e); + } + } + + private static Resource load(File file) + { + try + { + Resource resource = new UrlResource(file.toURI()); + if (resource.exists() || resource.isReadable()) + { + return resource; + } + else + { + throw new TransformException(500, "Could not read the target file: " + file.getPath()); + } + } + catch (MalformedURLException e) + { + throw new TransformException(500, "The target filename was malformed: " + file.getPath(), e); + } + } + + public static String getFilenameFromContentDisposition(HttpHeaders headers) + { + String filename = ""; + String contentDisposition = headers.getFirst(HttpHeaders.CONTENT_DISPOSITION); + if (contentDisposition != null) + { + String[] strings = contentDisposition.split("; *"); + filename = Arrays.stream(strings) + .filter(s -> s.startsWith(FILENAME)) + .findFirst() + .map(s -> s.substring(FILENAME.length())) + .orElse(""); + } + return filename; + } + + + /** + * Returns the file name for the target file + * + * @param fileName Desired file name + * @param targetExtension File extension + * @return Target file name + */ + public static String createTargetFileName(String fileName, String targetExtension) + { + String targetFilename = null; + String sourceFilename = fileName; + sourceFilename = StringUtils.getFilename(sourceFilename); + if (sourceFilename != null && !sourceFilename.isEmpty()) + { + String ext = StringUtils.getFilenameExtension(sourceFilename); + targetFilename = (ext != null && !ext.isEmpty() + ? sourceFilename.substring(0, sourceFilename.length()-ext.length()-1) + : sourceFilename)+ + '.'+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. + */ + public static File createSourceFile(HttpServletRequest request, MultipartFile multipartFile) + { + String filename = multipartFile.getOriginalFilename(); + long size = multipartFile.getSize(); + filename = checkFilename( true, filename); + File file = TempFileProvider.createTempFile("source_", "_" + filename); + request.setAttribute(SOURCE_FILE, file); + save(multipartFile, file); + LogEntry.setSource(filename, size); + return file; + } + + public static void deleteFile(HttpServletRequest request, String attributeName) + { + File file = (File) request.getAttribute(attributeName); + if (file != null) + { + file.delete(); + } + } + + public static ResponseEntity createAttachment(String targetFilename, File + targetFile) + { + Resource targetResource = load(targetFile); + targetFilename = UriUtils.encodePath(StringUtils.getFilename(targetFilename), "UTF-8"); + return ResponseEntity.ok().header(HttpHeaders + .CONTENT_DISPOSITION, + "attachment; filename*= UTF-8''" + targetFilename).body(targetResource); + } +} diff --git a/alfresco-transformer-base/src/main/java/org/alfresco/transformer/LogEntry.java b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/logging/LogEntry.java similarity index 93% rename from alfresco-transformer-base/src/main/java/org/alfresco/transformer/LogEntry.java rename to alfresco-transformer-base/src/main/java/org/alfresco/transformer/logging/LogEntry.java index a3a23d99..2d0fdbb2 100644 --- a/alfresco-transformer-base/src/main/java/org/alfresco/transformer/LogEntry.java +++ b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/logging/LogEntry.java @@ -23,7 +23,9 @@ * along with Alfresco. If not, see . * #L% */ -package org.alfresco.transformer; +package org.alfresco.transformer.logging; + +import static java.lang.Math.max; import java.text.SimpleDateFormat; import java.util.Collection; @@ -32,7 +34,8 @@ import java.util.Deque; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.atomic.AtomicInteger; -import static java.lang.Math.max; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; /** * Provides setter and getter methods to allow the current Thread to set various log properties and for these @@ -40,8 +43,9 @@ import static java.lang.Math.max; * 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 +public final class LogEntry { + private static final Log logger = LogFactory.getLog(LogEntry.class); // TODO allow ProbeTestTransform to find out if there are any transforms running longer than the max time. private static final AtomicInteger count = new AtomicInteger(0); @@ -49,20 +53,15 @@ public class LogEntry private static final int MAX_LOG_SIZE = 10; private static final SimpleDateFormat HH_MM_SS = new SimpleDateFormat("HH:mm:ss"); - private static ThreadLocal currentLogEntry = new ThreadLocal() - { - @Override - protected LogEntry initialValue() + private static final ThreadLocal currentLogEntry = ThreadLocal.withInitial(() -> { + LogEntry logEntry = new LogEntry(); + if (log.size() >= MAX_LOG_SIZE) { - LogEntry logEntry = new LogEntry(); - if (log.size() >= MAX_LOG_SIZE) - { - log.removeLast(); - } - log.addFirst(logEntry); - return logEntry; + log.removeLast(); } - }; + log.addFirst(logEntry); + return logEntry; + }); private final int id = count.incrementAndGet(); private final long start = System.currentTimeMillis(); @@ -204,9 +203,9 @@ public class LogEntry } currentLogEntry.remove(); - if (AbstractTransformerController.logger != null && AbstractTransformerController.logger.isDebugEnabled()) + if (logger.isDebugEnabled()) { - AbstractTransformerController.logger.debug(logEntry.toString()); + logger.debug(logEntry.toString()); } } @@ -279,6 +278,7 @@ public class LogEntry private String size(long size) { + // TODO fix numeric overflow in TB expression return size == -1 ? "" : size(size, "1 byte", new String[] { "bytes", " KB", " MB", " GB", " TB" }, new long[] { 1024, 1024*1024, 1024*1024*1024, 1024*1024*1024*1024, Long.MAX_VALUE }); diff --git a/alfresco-transformer-base/src/main/java/org/alfresco/transformer/logging/StandardMessages.java b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/logging/StandardMessages.java new file mode 100644 index 00000000..cf1fa76f --- /dev/null +++ b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/logging/StandardMessages.java @@ -0,0 +1,12 @@ +package org.alfresco.transformer.logging; + +public interface StandardMessages +{ + 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" ; +} diff --git a/alfresco-transformer-base/src/main/java/org/alfresco/transformer/ProbeTestTransform.java b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/probes/ProbeTestTransform.java similarity index 85% rename from alfresco-transformer-base/src/main/java/org/alfresco/transformer/ProbeTestTransform.java rename to alfresco-transformer-base/src/main/java/org/alfresco/transformer/probes/ProbeTestTransform.java index bc596fe1..748646e6 100644 --- a/alfresco-transformer-base/src/main/java/org/alfresco/transformer/ProbeTestTransform.java +++ b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/probes/ProbeTestTransform.java @@ -23,7 +23,10 @@ * along with Alfresco. If not, see . * #L% */ -package org.alfresco.transformer; +package org.alfresco.transformer.probes; + +import static org.alfresco.transformer.fs.FileManager.SOURCE_FILE; +import static org.alfresco.transformer.fs.FileManager.TARGET_FILE; import java.io.File; import java.io.IOException; @@ -35,6 +38,9 @@ import java.util.concurrent.atomic.AtomicLong; import javax.servlet.http.HttpServletRequest; +import org.alfresco.transformer.AbstractTransformerController; +import org.alfresco.transformer.exceptions.TransformException; +import org.alfresco.transformer.logging.LogEntry; import org.alfresco.util.TempFileProvider; import org.apache.commons.logging.Log; @@ -60,9 +66,9 @@ import org.apache.commons.logging.Log; *
  • maxTransformSeconds - the maximum time for a transformation, including failed ones.
  • * */ -abstract class ProbeTestTransform +public abstract class ProbeTestTransform { - public static final int AVERAGE_OVER_TRANSFORMS = 5; + private static final int AVERAGE_OVER_TRANSFORMS = 5; private final String sourceFilename; private final String targetFilename; private final long minExpectedLength; @@ -70,22 +76,32 @@ abstract class ProbeTestTransform private final Log logger; - int livenessPercent; - long probeCount; - int transCount; - long normalTime; - long maxTime = Long.MAX_VALUE; - long nextTransformTime; + 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 long maxTransformCount = Long.MAX_VALUE; + private final long maxTransformCount; private long maxTransformTime; - private AtomicBoolean initialised = new AtomicBoolean(false); - private AtomicBoolean readySent = new AtomicBoolean(false); - private AtomicLong transformCount = new AtomicLong(0); - private AtomicBoolean die = new AtomicBoolean(false); + 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; + } /** * See Probes.md for more info. @@ -97,12 +113,12 @@ abstract class ProbeTestTransform * @param maxTransformSeconds default values normally supplied by helm. Not identical so we can be sure which value is used. * @param livenessTransformPeriodSeconds default values normally supplied by helm. Not identical so we can be sure which value is used. */ - public ProbeTestTransform(AbstractTransformerController controller, + public ProbeTestTransform(AbstractTransformerController controller, Log logger, String sourceFilename, String targetFilename, long expectedLength, long plusOrMinus, int livenessPercent, long maxTransforms, long maxTransformSeconds, long livenessTransformPeriodSeconds) { - logger = controller.logger; + this.logger = logger; this.sourceFilename = sourceFilename; this.targetFilename = targetFilename; @@ -128,7 +144,7 @@ abstract class ProbeTestTransform return defaultValue; } - protected long getPositiveLongEnv(String name, long defaultValue) + private long getPositiveLongEnv(String name, long defaultValue) { long l = -1; String env = System.getenv(name); @@ -171,7 +187,7 @@ abstract class ProbeTestTransform { String probeMessage = getProbeMessage(isLiveProbe); String message = "Success - No transform."; - LogEntry.setStatusCodeAndMessage(200, probeMessage+message); + LogEntry.setStatusCodeAndMessage(200, probeMessage + message); if (!isLiveProbe && !readySent.getAndSet(true)) { logger.info(probeMessage+message); @@ -179,7 +195,7 @@ abstract class ProbeTestTransform return message; } - String doTransform(HttpServletRequest request, boolean isLiveProbe) + private String doTransform(HttpServletRequest request, boolean isLiveProbe) { checkMaxTransformTimeAndCount(isLiveProbe); @@ -207,9 +223,9 @@ abstract class ProbeTestTransform if (time > maxTime) { - throw new TransformException(500, getMessagePrefix(isLiveProbe)+ - message+" which is more than "+ livenessPercent + - "% slower than the normal value of "+normalTime+"ms"); + throw new TransformException(500, 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. @@ -237,11 +253,11 @@ abstract class ProbeTestTransform } } - File getSourceFile(HttpServletRequest request, boolean isLiveProbe) + private File getSourceFile(HttpServletRequest request, boolean isLiveProbe) { incrementTransformerCount(); File sourceFile = TempFileProvider.createTempFile("source_", "_"+ sourceFilename); - request.setAttribute(AbstractTransformerController.SOURCE_FILE, sourceFile); + request.setAttribute(SOURCE_FILE, sourceFile); try (InputStream inputStream = this.getClass().getResourceAsStream('/'+sourceFilename)) { Files.copy(inputStream, sourceFile.toPath(), StandardCopyOption.REPLACE_EXISTING); @@ -256,15 +272,15 @@ abstract class ProbeTestTransform return sourceFile; } - File getTargetFile(HttpServletRequest request) + private File getTargetFile(HttpServletRequest request) { File targetFile = TempFileProvider.createTempFile("target_", "_"+targetFilename); - request.setAttribute(AbstractTransformerController.TARGET_FILE, targetFile); + request.setAttribute(TARGET_FILE, targetFile); LogEntry.setTarget(targetFilename); return targetFile; } - void recordTransformTime(long time) + public void recordTransformTime(long time) { if (maxTransformTime > 0 && time > maxTransformTime) { @@ -272,7 +288,7 @@ abstract class ProbeTestTransform } } - void calculateMaxTime(long time, boolean isLiveProbe) + public void calculateMaxTime(long time, boolean isLiveProbe) { if (transCount <= AVERAGE_OVER_TRANSFORMS) { @@ -331,4 +347,15 @@ abstract class ProbeTestTransform { transformCount.incrementAndGet(); } + + public void setLivenessPercent(int livenessPercent) + { + this.livenessPercent = livenessPercent; + } + + public long getNormalTime() + { + + return normalTime; + } } diff --git a/alfresco-transformer-base/src/main/java/org/alfresco/transformer/util/Util.java b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/util/Util.java new file mode 100644 index 00000000..1aa540a4 --- /dev/null +++ b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/util/Util.java @@ -0,0 +1,28 @@ +package org.alfresco.transformer.util; + +/** + */ +public class 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(String param) + { + return param == null ? null : Integer.parseInt(param); + } + + /** + * 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 Boolean} + */ + public static Boolean stringToBoolean(String param) + { + return param == null? null : Boolean.parseBoolean(param); + } +} diff --git a/alfresco-transformer-base/src/test/java/org/alfresco/transformer/AbstractHttpRequestTest.java b/alfresco-transformer-base/src/test/java/org/alfresco/transformer/AbstractHttpRequestTest.java index 47e5343a..fe3e46f5 100644 --- a/alfresco-transformer-base/src/test/java/org/alfresco/transformer/AbstractHttpRequestTest.java +++ b/alfresco-transformer-base/src/test/java/org/alfresco/transformer/AbstractHttpRequestTest.java @@ -32,8 +32,6 @@ import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.http.*; import org.springframework.util.LinkedMultiValueMap; -import java.io.IOException; - import static org.junit.Assert.assertEquals; import static org.springframework.test.util.AssertionErrors.assertTrue; @@ -54,7 +52,7 @@ public abstract class AbstractHttpRequestTest protected abstract String getSourceExtension(); @Test - public void testPageExists() throws Exception + public void testPageExists() { String result = restTemplate.getForObject("http://localhost:" + port + "/", String.class); @@ -63,7 +61,7 @@ public abstract class AbstractHttpRequestTest } @Test - public void logPageExists() throws Exception + public void logPageExists() { String result = restTemplate.getForObject("http://localhost:" + port + "/log", String.class); @@ -72,7 +70,7 @@ public abstract class AbstractHttpRequestTest } @Test - public void errorPageExists() throws Exception + public void errorPageExists() { String result = restTemplate.getForObject("http://localhost:" + port + "/error", String.class); @@ -81,7 +79,7 @@ public abstract class AbstractHttpRequestTest } @Test - public void noFileError() throws Exception + public void noFileError() { // Transformer name is not part of the title as this is checked by another handler assertTransformError(false, @@ -94,22 +92,22 @@ public abstract class AbstractHttpRequestTest assertMissingParameter("targetExtension"); } - protected void assertMissingParameter(String name) throws IOException + private void assertMissingParameter(String name) { assertTransformError(true, getTransformerName() + " - Request parameter " + name + " is missing"); } - protected void assertTransformError(boolean addFile, String errorMessage) throws IOException + private void assertTransformError(boolean addFile, String errorMessage) { - LinkedMultiValueMap parameters = new LinkedMultiValueMap(); + LinkedMultiValueMap parameters = new LinkedMultiValueMap<>(); if (addFile) { parameters.add("file", new org.springframework.core.io.ClassPathResource("quick."+getSourceExtension())); } HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.MULTIPART_FORM_DATA); - HttpEntity> entity = new HttpEntity>(parameters, headers); + HttpEntity> entity = new HttpEntity<>(parameters, headers); ResponseEntity response = restTemplate.exchange("/transform", HttpMethod.POST, entity, String.class, ""); assertEquals(errorMessage, getErrorMessage(response.getBody())); } @@ -117,7 +115,7 @@ public abstract class AbstractHttpRequestTest // Strip out just the error message from the returned json content body // Had been expecting the Error page to be returned, but we end up with the json in this test harness. // Is correct if run manually, so not worrying too much about this. - private String getErrorMessage(String content) throws IOException + private String getErrorMessage(String content) { String message = ""; int i = content.indexOf("\"message\":\""); diff --git a/alfresco-transformer-base/src/test/java/org/alfresco/transformer/AbstractTransformerControllerTest.java b/alfresco-transformer-base/src/test/java/org/alfresco/transformer/AbstractTransformerControllerTest.java index bef4562a..9e0abe8d 100644 --- a/alfresco-transformer-base/src/test/java/org/alfresco/transformer/AbstractTransformerControllerTest.java +++ b/alfresco-transformer-base/src/test/java/org/alfresco/transformer/AbstractTransformerControllerTest.java @@ -27,12 +27,7 @@ package org.alfresco.transformer; import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyLong; -import static org.mockito.Matchers.anyObject; -import static org.mockito.Mockito.when; 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; @@ -44,33 +39,21 @@ import java.io.IOException; import java.net.URL; import java.nio.channels.FileChannel; import java.nio.file.Files; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; import org.alfresco.transform.client.model.TransformReply; import org.alfresco.transform.client.model.TransformRequest; -import org.alfresco.transformer.model.FileRefEntity; -import org.alfresco.transformer.model.FileRefResponse; -import org.alfresco.util.exec.RuntimeExec; -import org.junit.Before; +import org.alfresco.transformer.clients.AlfrescoSharedFileStoreClient; +import org.alfresco.transformer.probes.ProbeTestTransform; import org.junit.Test; -import org.mockito.Mock; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.io.FileSystemResource; -import org.springframework.core.io.Resource; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.util.StringUtils; import com.fasterxml.jackson.databind.ObjectMapper; @@ -85,18 +68,9 @@ public abstract class AbstractTransformerControllerTest @Autowired protected ObjectMapper objectMapper; - @Mock - private RuntimeExec mockTransformCommand; - - @Mock - private RuntimeExec mockCheckCommand; - - @Mock + @MockBean protected AlfrescoSharedFileStoreClient alfrescoSharedFileStoreClient; - @Mock - private RuntimeExec.ExecutionResult mockExecutionResult; - protected String sourceExtension; protected String targetExtension; protected String sourceMimetype; @@ -108,88 +82,14 @@ public abstract class AbstractTransformerControllerTest protected byte[] expectedSourceFileBytes; protected byte[] expectedTargetFileBytes; - protected AbstractTransformerController controller; - - @Before - public void before() throws Exception - { - } - // Called by sub class - public void mockTransformCommand(AbstractTransformerController controller, String sourceExtension, - String targetExtension, String sourceMimetype, - boolean readTargetFileBytes) throws IOException - { - this.controller = controller; - this.sourceExtension = sourceExtension; - this.targetExtension = targetExtension; - this.sourceMimetype = sourceMimetype; + protected abstract void mockTransformCommand(String sourceExtension, + String targetExtension, String sourceMimetype, + boolean readTargetFileBytes) throws IOException; - expectedOptions = null; - expectedSourceSuffix = null; - expectedSourceFileBytes = readTestFile(sourceExtension); - expectedTargetFileBytes = readTargetFileBytes ? readTestFile(targetExtension) : null; - sourceFile = new MockMultipartFile("file", "quick."+sourceExtension, sourceMimetype, expectedSourceFileBytes); + protected abstract AbstractTransformerController getController(); - controller.setTransformCommand(mockTransformCommand); - controller.setCheckCommand(mockCheckCommand); - - when(mockTransformCommand.execute(anyObject(), anyLong())).thenAnswer(new Answer() - { - public RuntimeExec.ExecutionResult answer(InvocationOnMock invocation) throws Throwable - { - Map actualProperties = invocation.getArgument(0); - assertEquals("There should be 3 properties", 3, actualProperties.size()); - - String actualOptions = actualProperties.get("options"); - String actualSource = actualProperties.get("source"); - String actualTarget = actualProperties.get("target"); - String actualTargetExtension = StringUtils.getFilenameExtension(actualTarget); - - assertNotNull(actualSource); - assertNotNull(actualTarget); - if (expectedSourceSuffix != null) - { - assertTrue("The source file \""+actualSource+"\" should have ended in \""+expectedSourceSuffix+"\"", actualSource.endsWith(expectedSourceSuffix)); - actualSource = actualSource.substring(0, actualSource.length()-expectedSourceSuffix.length()); - } - - assertNotNull(actualOptions); - if (expectedOptions != null) - { - assertEquals("expectedOptions", expectedOptions, actualOptions); - } - - Long actualTimeout = invocation.getArgument(1); - assertNotNull(actualTimeout); - if (expectedTimeout != null) - { - assertEquals("expectedTimeout", expectedTimeout, actualTimeout); - } - - // Copy a test file into the target file location if it exists - int i = actualTarget.lastIndexOf('_'); - if (i >= 0) - { - String testFilename = actualTarget.substring(i+1); - File testFile = getTestFile(testFilename, false); - File targetFile = new File(actualTarget); - generateTargetFileFromResourceFile(actualTargetExtension, testFile, - targetFile); - } - - // Check the supplied source file has not been changed. - byte[] actualSourceFileBytes = Files.readAllBytes(new File(actualSource).toPath()); - assertTrue("Source file is not the same", Arrays.equals(expectedSourceFileBytes, actualSourceFileBytes)); - - return mockExecutionResult; - } - }); - - when(mockExecutionResult.getExitValue()).thenReturn(0); - when(mockExecutionResult.getStdErr()).thenReturn("STDERROR"); - when(mockExecutionResult.getStdOut()).thenReturn("STDOUT"); - } + protected abstract void updateTransformRequestWithSpecificOptions(TransformRequest transformRequest); /** * This method ends up being the core of the mock. @@ -197,8 +97,8 @@ public abstract class AbstractTransformerControllerTest * 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 + * @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. */ void generateTargetFileFromResourceFile(String actualTargetExtension, File testFile, @@ -224,7 +124,7 @@ public abstract class AbstractTransformerControllerTest protected byte[] readTestFile(String extension) throws IOException { - return Files.readAllBytes(getTestFile("quick."+extension, true).toPath()); + return Files.readAllBytes(getTestFile("quick." + extension, true).toPath()); } protected File getTestFile(String testFilename, boolean required) throws IOException @@ -233,22 +133,22 @@ public abstract class AbstractTransformerControllerTest URL testFileUrl = classLoader.getResource(testFilename); if (required && testFileUrl == null) { - throw new IOException("The test file "+testFilename+" does not exist in the resources directory"); + throw new IOException("The test file " + testFilename + " does not exist in the resources directory"); } return testFileUrl == null ? null : new File(testFileUrl.getFile()); } protected MockHttpServletRequestBuilder mockMvcRequest(String url, MockMultipartFile sourceFile, String... params) { - MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.fileUpload("/transform").file(sourceFile); + MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.multipart("/transform").file(sourceFile); if (params.length % 2 != 0) { throw new IllegalArgumentException("each param should have a name and value."); } - for (int i=0; i= 400); - assertTrue("Delay sending the result back was too big "+ms, ms <= 500); + .andExpect(status().is(200)) + .andExpect(content().bytes(expectedTargetFileBytes)) + .andExpect(header().string("Content-Disposition", "attachment; filename*= UTF-8''quick." + targetExtension)); + long ms = System.currentTimeMillis() - start; + System.out.println("Transform incluing test delay was " + ms); + assertTrue("Delay sending the result back was too small " + ms, ms >= 400); + assertTrue("Delay sending the result back was too big " + ms, ms <= 500); } @Test public void noTargetFileTest() throws Exception { mockMvc.perform(mockMvcRequest("/transform", sourceFile, "targetExtension", "xxx")) - .andExpect(status().is(500)); - } - - @Test - public void badExitCodeTest() throws Exception - { - when(mockExecutionResult.getExitValue()).thenReturn(1); - - mockMvc.perform(mockMvcRequest("/transform", sourceFile, "targetExtension", "xxx")) - .andExpect(status().is(400)) - .andExpect(status().reason(containsString("Transformer exit code was not 0: \nSTDERR"))); + .andExpect(status().is(500)); } @Test // Looks dangerous but is okay as we only use the final filename public void dotDotSourceFilenameTest() throws Exception { - sourceFile = new MockMultipartFile("file", "../quick."+sourceExtension, sourceMimetype, expectedSourceFileBytes); + sourceFile = new MockMultipartFile("file", "../quick." + sourceExtension, sourceMimetype, expectedSourceFileBytes); mockMvc.perform(mockMvcRequest("/transform", sourceFile, "targetExtension", targetExtension)) - .andExpect(status().is(200)) - .andExpect(content().bytes(expectedTargetFileBytes)) - .andExpect(header().string("Content-Disposition", "attachment; filename*= UTF-8''quick."+targetExtension)); + .andExpect(status().is(200)) + .andExpect(content().bytes(expectedTargetFileBytes)) + .andExpect(header().string("Content-Disposition", "attachment; filename*= UTF-8''quick." + targetExtension)); } @Test @@ -313,9 +203,9 @@ public abstract class AbstractTransformerControllerTest sourceFile = new MockMultipartFile("file", "../quick", sourceMimetype, expectedSourceFileBytes); mockMvc.perform(mockMvcRequest("/transform", sourceFile, "targetExtension", targetExtension)) - .andExpect(status().is(200)) - .andExpect(content().bytes(expectedTargetFileBytes)) - .andExpect(header().string("Content-Disposition", "attachment; filename*= UTF-8''quick."+targetExtension)); + .andExpect(status().is(200)) + .andExpect(content().bytes(expectedTargetFileBytes)) + .andExpect(header().string("Content-Disposition", "attachment; filename*= UTF-8''quick." + targetExtension)); } @Test @@ -325,8 +215,8 @@ public abstract class AbstractTransformerControllerTest sourceFile = new MockMultipartFile("file", "abc/", sourceMimetype, expectedSourceFileBytes); mockMvc.perform(mockMvcRequest("/transform", sourceFile, "targetExtension", targetExtension)) - .andExpect(status().is(400)) - .andExpect(status().reason(containsString("The source filename was not supplied"))); + .andExpect(status().is(400)) + .andExpect(status().reason(containsString("The source filename was not supplied"))); } @Test @@ -335,113 +225,47 @@ public abstract class AbstractTransformerControllerTest sourceFile = new MockMultipartFile("file", "", sourceMimetype, expectedSourceFileBytes); mockMvc.perform(mockMvcRequest("/transform", sourceFile, "targetExtension", targetExtension)) - .andExpect(status().is(400)) - .andExpect(status().reason(containsString("The source filename was not supplied"))); + .andExpect(status().is(400)) + .andExpect(status().reason(containsString("The source filename was not supplied"))); } @Test public void noTargetExtensionTest() throws Exception { mockMvc.perform(mockMvcRequest("/transform", sourceFile)) - .andExpect(status().is(400)) - .andExpect(status().reason(containsString("Request parameter targetExtension is missing"))); + .andExpect(status().is(400)) + .andExpect(status().reason(containsString("Request parameter targetExtension is missing"))); } -// @Test -// // Not a real test, but helpful for trying out the duration times in log code. -// public void testTimes() throws InterruptedException -// { -// LogEntry.start(); -// Thread.sleep(50); -// LogEntry.setSource("test File", 1234); -// Thread.sleep(200); -// LogEntry.setStatusCodeAndMessage(200, "Success"); -// LogEntry.addDelay(2000L); -// for (LogEntry logEntry: LogEntry.getLog()) -// { -// String str = logEntry.getDuration(); -// System.out.println(str); -// } -// } - @Test public void calculateMaxTime() throws Exception { - ProbeTestTransform probeTestTransform = controller.getProbeTestTransform(); - probeTestTransform.livenessPercent = 110; + ProbeTestTransform probeTestTransform = getController().getProbeTestTransform(); + probeTestTransform.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} + 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) + for (long[] v : values) { long time = v[0]; long expectedNormalTime = v[1]; long expectedMaxTime = v[2]; probeTestTransform.calculateMaxTime(time, true); - assertEquals("", expectedNormalTime, probeTestTransform.normalTime); - assertEquals("", expectedMaxTime, probeTestTransform.maxTime); + assertEquals("", expectedNormalTime, probeTestTransform.getNormalTime()); + assertEquals("", expectedMaxTime, probeTestTransform.getMaxTime()); } } - @Test - public void testPojoTransform() throws Exception - { - // Files - String sourceFileRef = UUID.randomUUID().toString(); - File sourceFile = getTestFile("quick." + sourceExtension, true); - String targetFileRef = UUID.randomUUID().toString(); - - - // Transformation Request POJO - TransformRequest transformRequest = new TransformRequest(); - transformRequest.setRequestId("1"); - transformRequest.setSchema(1); - transformRequest.setClientData("Alfresco Digital Business Platform"); - transformRequest.setTransformRequestOptions(new HashMap<>()); - transformRequest.setSourceReference(sourceFileRef); - transformRequest.setSourceExtension(sourceExtension); - transformRequest.setSourceSize(sourceFile.length()); - transformRequest.setTargetExtension(targetExtension); - - // HTTP Request - HttpHeaders headers = new HttpHeaders(); - headers.set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=quick." + sourceExtension); - ResponseEntity response = new ResponseEntity<>(new FileSystemResource( - sourceFile), headers, HttpStatus.OK); - - when(alfrescoSharedFileStoreClient.retrieveFile(sourceFileRef)).thenReturn(response); - when(alfrescoSharedFileStoreClient.saveFile(any())).thenReturn(new FileRefResponse(new FileRefEntity(targetFileRef))); - when(mockExecutionResult.getExitValue()).thenReturn(0); - - // 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("/transform") - .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).content(tr)) - .andExpect(status().is(HttpStatus.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 testEmptyPojoTransform() throws Exception { @@ -450,9 +274,12 @@ public abstract class AbstractTransformerControllerTest // Serialize and call the transformer String tr = objectMapper.writeValueAsString(transformRequest); - String transformationReplyAsString = mockMvc.perform(MockMvcRequestBuilders.post("/transform") - .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).content(tr)) + String transformationReplyAsString = mockMvc + .perform(MockMvcRequestBuilders + .post("/transform") + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .content(tr)) .andExpect(status().is(HttpStatus.BAD_REQUEST.value())) .andReturn().getResponse().getContentAsString(); @@ -461,6 +288,4 @@ public abstract class AbstractTransformerControllerTest // Assert the reply assertEquals(HttpStatus.BAD_REQUEST.value(), transformReply.getStatus()); } - - protected abstract void updateTransformRequestWithSpecificOptions(TransformRequest transformRequest); } diff --git a/pom.xml b/pom.xml index 42c4a33d..91161e7d 100644 --- a/pom.xml +++ b/pom.xml @@ -3,9 +3,10 @@ 4.0.0 - org.alfresco - alfresco-super-pom - 9 + org.springframework.boot + spring-boot-starter-parent + 2.0.5.RELEASE + org.alfresco @@ -44,13 +45,6 @@ - - org.springframework.boot - spring-boot-starter-parent - ${dependency.spring-boot.version} - pom - import - ch.qos.logback logback-core @@ -121,4 +115,22 @@ + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.0.1 + + + attach-javadocs + + jar + + + + + + +