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..48ec051d 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,24 @@ */ 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.springframework.http.HttpStatus.OK; + 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 +62,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 +127,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 +145,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(OK.value(), "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..1d416be9 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 @@ -11,7 +11,6 @@ */ package org.alfresco.transformer; -import io.micrometer.core.instrument.MeterRegistry; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; @@ -20,6 +19,8 @@ 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 +28,11 @@ public class Application @Value("${container.name}") private String containerName; - @Bean MeterRegistryCustomizer metricsCommonTags() { + @Bean + public MeterRegistryCustomizer metricsCommonTags() { return registry -> registry.config().commonTags("containerName", containerName); } - + public static void main(String[] args) { SpringApplication.run(Application.class, 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..3eb76a95 --- /dev/null +++ b/alfresco-docker-alfresco-pdf-renderer/src/main/java/org/alfresco/transformer/executors/PdfRendererCommandExecutor.java @@ -0,0 +1,45 @@ +package org.alfresco.transformer.executors; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.util.exec.RuntimeExec; +import org.springframework.stereotype.Component; + +/** + * CommandExecutor implementation for running PDF Renderer transformations. It runs the + * transformation logic as a separate Shell process. + */ +@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..51f50afe 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,50 @@ */ 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.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.OK; 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 +78,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) @@ -73,7 +193,7 @@ public class AlfrescoPdfRendererControllerTest extends AbstractTransformerContro .param("allowEnlargement", "true") .param("maintainAspectRatio", "true")) - .andExpect(status().is(200)) + .andExpect(status().is(OK.value())) .andExpect(content().bytes(expectedTargetFileBytes)) .andExpect(header().string("Content-Disposition", "attachment; filename*= UTF-8''quick."+targetExtension)); } @@ -82,7 +202,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) @@ -93,7 +213,7 @@ public class AlfrescoPdfRendererControllerTest extends AbstractTransformerContro .param("allowEnlargement", "false") .param("maintainAspectRatio", "false")) - .andExpect(status().is(200)) + .andExpect(status().is(OK.value())) .andExpect(content().bytes(expectedTargetFileBytes)) .andExpect(header().string("Content-Disposition", "attachment; filename*= UTF-8''quick."+targetExtension)); } @@ -106,4 +226,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(BAD_REQUEST.value())) + .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, 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..8aca3520 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 @@ -11,15 +11,16 @@ */ package org.alfresco.transformer; -import io.micrometer.core.instrument.MeterRegistry; 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 +28,11 @@ public class Application @Value("${container.name}") private String containerName; - @Bean MeterRegistryCustomizer metricsCommonTags() { + @Bean + MeterRegistryCustomizer metricsCommonTags() { return registry -> registry.config().commonTags("containerName", containerName); } - + public static void main(String[] args) { SpringApplication.run(Application.class, 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 e2ff71f8..520e6587 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,24 @@ */ 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.stringToInteger; +import static org.springframework.http.HttpStatus.OK; + 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.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 +64,43 @@ 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 List GRAVITY_VALUES = Arrays.asList( - "North", "NorthEast", "East", "SouthEast", "South", "SouthWest", "West", "NorthWest", "Center"); + private static final Log logger = LogFactory.getLog(ImageMagickController.class); + @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,176 +146,80 @@ 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 - String options = buildTransformOptions(startPage, endPage , alphaRemove, autoOrient, cropGravity, cropWidth, cropHeight, cropPercentage, - cropXOffset, cropYOffset, thumbnail, resizeWidth, resizeHeight, resizePercentage, allowEnlargement, maintainAspectRatio, commandOptions); + final String options = OptionsBuilder + .builder() + .withStartPage(startPage) + .withEndPage(endPage) + .withAlphaRemove(alphaRemove) + .withAutoOrient(autoOrient) + .withCropGravity(cropGravity) + .withCropWidth(cropWidth) + .withCropHeight(cropHeight) + .withCropPercentage(cropPercentage) + .withCropXOffset(cropXOffset) + .withCropYOffset(cropYOffset) + .withThumbnail(thumbnail) + .withResizeWidth(resizeWidth) + .withResizeHeight(resizeHeight) + .withResizePercentage(resizePercentage) + .withAllowEnlargement(allowEnlargement) + .withMaintainAspectRatio(maintainAspectRatio) + .withCommandOptions(commandOptions) + .build(); + 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(OK.value(), "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")); - Boolean alphaRemove = stringToBoolean(transformOptions.get("alphaRemove")); - Boolean autoOrient = stringToBoolean(transformOptions.get("autoOrient")); - String cropGravity = transformOptions.get("cropGravity"); - Integer cropWidth = stringToInteger(transformOptions.get("cropWidth")); - Integer cropHeight = stringToInteger(transformOptions.get("cropHeight")); - Boolean cropPercentage = stringToBoolean(transformOptions.get("cropPercentage")); - Integer cropXOffset = stringToInteger(transformOptions.get("cropXOffset")); - Integer cropYOffset = stringToInteger(transformOptions.get("cropYOffset")); - Boolean thumbnail = stringToBoolean(transformOptions.get("thumbnail")); - Integer resizeWidth = stringToInteger(transformOptions.get("resizeWidth")); - Integer resizeHeight = stringToInteger(transformOptions.get("resizeHeight")); - Boolean resizePercentage = stringToBoolean(transformOptions.get("resizePercentage")); - Boolean allowEnlargement = stringToBoolean(transformOptions.get("allowEnlargement")); - Boolean maintainAspectRatio = stringToBoolean(transformOptions.get("maintainAspectRatio")); + final String options = OptionsBuilder + .builder() + .withStartPage(transformOptions.get("startPage")) + .withEndPage(transformOptions.get("endPage")) + .withAlphaRemove(transformOptions.get("alphaRemove")) + .withAutoOrient(transformOptions.get("autoOrient")) + .withCropGravity(transformOptions.get("cropGravity")) + .withCropWidth(transformOptions.get("cropWidth")) + .withCropHeight(transformOptions.get("cropHeight")) + .withCropPercentage(transformOptions.get("cropPercentage")) + .withCropXOffset(transformOptions.get("cropXOffset")) + .withCropYOffset(transformOptions.get("cropYOffset")) + .withThumbnail(transformOptions.get("thumbnail")) + .withResizeWidth(transformOptions.get("resizeWidth")) + .withResizeHeight(transformOptions.get("resizeHeight")) + .withResizePercentage(transformOptions.get("resizePercentage")) + .withAllowEnlargement(transformOptions.get("allowEnlargement")) + .withMaintainAspectRatio(transformOptions.get("maintainAspectRatio")) + .build(); - String options = buildTransformOptions(startPage, endPage , alphaRemove, autoOrient, cropGravity, cropWidth, cropHeight, cropPercentage, - cropXOffset, cropYOffset, thumbnail, resizeWidth, resizeHeight, resizePercentage, allowEnlargement, maintainAspectRatio, null); - String pageRange = calculatePageRange(startPage, endPage); + final String pageRange = calculatePageRange( + stringToInteger(transformOptions.get("startPage")), + stringToInteger(transformOptions.get("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) + private static String calculatePageRange(Integer startPage, Integer endPage) { - 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, - Boolean autoOrient, String cropGravity, Integer cropWidth, Integer cropHeight, - Boolean cropPercentage, Integer cropXOffset, Integer cropYOffset, Boolean thumbnail, - Integer resizeWidth, Integer resizeHeight, Boolean resizePercentage, - Boolean allowEnlargement, Boolean maintainAspectRatio, String commandOptions) - { - if (cropGravity != null) - { - cropGravity = cropGravity.trim(); - if (cropGravity.isEmpty()) - { - cropGravity = null; - } - else if (!GRAVITY_VALUES.contains(cropGravity)) - { - throw new TransformException(400, "Invalid cropGravity value"); - } - } - - StringJoiner args = new StringJoiner(" "); - if (alphaRemove != null && alphaRemove) - { - args.add("-alpha"); - args.add(("remove")); - } - if (autoOrient != null && autoOrient) - { - args.add("-auto-orient"); - } - - if (cropGravity != null || cropWidth != null || cropHeight != null || cropPercentage != null || - cropXOffset != null || cropYOffset != null) - { - if (cropGravity != null) - { - args.add("-gravity"); - args.add(cropGravity); - } - - StringBuilder crop = new StringBuilder(""); - if (cropWidth != null && cropWidth >= 0) - { - crop.append(cropWidth); - } - if (cropHeight != null && cropHeight >= 0) - { - crop.append('x'); - crop.append(cropHeight); - } - if (cropPercentage != null && cropPercentage) - { - crop.append('%'); - } - if (cropXOffset != null) - { - if (cropXOffset >= 0) - { - crop.append('+'); - } - crop.append(cropXOffset); - } - if (cropYOffset != null) - { - if (cropYOffset >= 0) - { - crop.append('+'); - } - crop.append(cropYOffset); - } - if (crop.length() > 1) - { - args.add("-crop"); - args.add(crop); - } - - args.add("+repage"); - } - - if (resizeHeight != null || resizeWidth != null || resizePercentage !=null || maintainAspectRatio != null) - { - args.add(thumbnail != null && thumbnail ? "-thumbnail" : "-resize"); - StringBuilder resize = new StringBuilder(""); - if (resizeWidth != null && resizeWidth >= 0) - { - resize.append(resizeWidth); - } - if (resizeHeight != null && resizeHeight >= 0) - { - resize.append('x'); - resize.append(resizeHeight); - } - if (resizePercentage != null && resizePercentage) - { - resize.append('%'); - } - if (allowEnlargement == null || !allowEnlargement) - { - resize.append('>'); - } - if (maintainAspectRatio != null && maintainAspectRatio) - { - resize.append('!'); - } - if (resize.length() > 1) - { - args.add(resize); - } - } - - return (commandOptions == null || "".equals(commandOptions.trim()) ? "" : commandOptions + ' ') + - args.toString(); - } - - private String calculatePageRange(Integer startPage, Integer endPage) - { - return - startPage == null + return startPage == null ? endPage == null ? "" : "["+endPage+']' @@ -345,5 +227,4 @@ public class ImageMagickController extends AbstractTransformerController ? "["+startPage+']' : "["+startPage+'-'+endPage+']'; } - } diff --git a/alfresco-docker-imagemagick/src/main/java/org/alfresco/transformer/OptionsBuilder.java b/alfresco-docker-imagemagick/src/main/java/org/alfresco/transformer/OptionsBuilder.java new file mode 100644 index 00000000..589c4b69 --- /dev/null +++ b/alfresco-docker-imagemagick/src/main/java/org/alfresco/transformer/OptionsBuilder.java @@ -0,0 +1,336 @@ +package org.alfresco.transformer; + +import static java.util.Arrays.asList; +import static org.alfresco.transformer.util.Util.stringToBoolean; +import static org.alfresco.transformer.util.Util.stringToInteger; +import static org.springframework.http.HttpStatus.BAD_REQUEST; + +import java.util.List; +import java.util.StringJoiner; + +import org.alfresco.transformer.exceptions.TransformException; + +/** + * ImageMagick options builder. + * + * @author Cezar Leahu + */ +public final class OptionsBuilder +{ + private static final List GRAVITY_VALUES = asList("North", "NorthEast", "East", + "SouthEast", "South", "SouthWest", "West", "NorthWest", "Center"); + + private Integer startPage; + private Integer endPage; + private Boolean alphaRemove; + private Boolean autoOrient; + private String cropGravity; + private Integer cropWidth; + private Integer cropHeight; + private Boolean cropPercentage; + private Integer cropXOffset; + private Integer cropYOffset; + private Boolean thumbnail; + private Integer resizeWidth; + private Integer resizeHeight; + private Boolean resizePercentage; + private Boolean allowEnlargement; + private Boolean maintainAspectRatio; + private String commandOptions; + + private OptionsBuilder() + { + } + + public OptionsBuilder withStartPage(final String startPage) + { + return withStartPage(stringToInteger(startPage)); + } + + public OptionsBuilder withStartPage(final Integer startPage) + { + this.startPage = startPage; + return this; + } + + public OptionsBuilder withEndPage(final String endPage) + { + return withEndPage(stringToInteger(endPage)); + } + + public OptionsBuilder withEndPage(final Integer endPage) + { + this.endPage = endPage; + return this; + } + + public OptionsBuilder withAlphaRemove(final String alphaRemove) + { + return withAlphaRemove(stringToBoolean(alphaRemove)); + } + + public OptionsBuilder withAlphaRemove(final Boolean alphaRemove) + { + this.alphaRemove = alphaRemove; + return this; + } + + public OptionsBuilder withAutoOrient(final String autoOrient) + { + return withAutoOrient(stringToBoolean(autoOrient)); + } + + public OptionsBuilder withAutoOrient(final Boolean autoOrient) + { + this.autoOrient = autoOrient; + return this; + } + + public OptionsBuilder withCropGravity(final String cropGravity) + { + this.cropGravity = cropGravity; + return this; + } + + public OptionsBuilder withCropWidth(final String cropWidth) + { + return withCropWidth(stringToInteger(cropWidth)); + } + + public OptionsBuilder withCropWidth(final Integer cropWidth) + { + this.cropWidth = cropWidth; + return this; + } + + public OptionsBuilder withCropHeight(final String cropHeight) + { + return withCropHeight(stringToInteger(cropHeight)); + } + + public OptionsBuilder withCropHeight(final Integer cropHeight) + { + this.cropHeight = cropHeight; + return this; + } + + public OptionsBuilder withCropPercentage(final String cropPercentage) + { + return withCropPercentage(stringToBoolean(cropPercentage)); + } + + public OptionsBuilder withCropPercentage(final Boolean cropPercentage) + { + this.cropPercentage = cropPercentage; + return this; + } + + public OptionsBuilder withCropXOffset(final String cropXOffset) + { + return withCropXOffset(stringToInteger(cropXOffset)); + } + + public OptionsBuilder withCropXOffset(final Integer cropXOffset) + { + this.cropXOffset = cropXOffset; + return this; + } + + public OptionsBuilder withCropYOffset(final String cropYOffset) + { + return withCropYOffset(stringToInteger(cropYOffset)); + } + + public OptionsBuilder withCropYOffset(final Integer cropYOffset) + { + this.cropYOffset = cropYOffset; + return this; + } + + public OptionsBuilder withThumbnail(final String thumbnail) + { + return withThumbnail(stringToBoolean(thumbnail)); + } + + public OptionsBuilder withThumbnail(final Boolean thumbnail) + { + this.thumbnail = thumbnail; + return this; + } + + public OptionsBuilder withResizeWidth(final String resizeWidth) + { + return withResizeWidth(stringToInteger(resizeWidth)); + } + + public OptionsBuilder withResizeWidth(final Integer resizeWidth) + { + this.resizeWidth = resizeWidth; + return this; + } + + public OptionsBuilder withResizeHeight(final String resizeHeight) + { + return withResizeHeight(stringToInteger(resizeHeight)); + } + + public OptionsBuilder withResizeHeight(final Integer resizeHeight) + { + this.resizeHeight = resizeHeight; + return this; + } + + public OptionsBuilder withResizePercentage(final String resizePercentage) + { + return withResizePercentage(stringToBoolean(resizePercentage)); + } + + public OptionsBuilder withResizePercentage(final Boolean resizePercentage) + { + this.resizePercentage = resizePercentage; + return this; + } + + public OptionsBuilder withAllowEnlargement(final String allowEnlargement) + { + return withAllowEnlargement(stringToBoolean(allowEnlargement)); + } + + public OptionsBuilder withAllowEnlargement(final Boolean allowEnlargement) + { + this.allowEnlargement = allowEnlargement; + return this; + } + + public OptionsBuilder withMaintainAspectRatio(final String maintainAspectRatio) + { + return withMaintainAspectRatio(stringToBoolean(maintainAspectRatio)); + } + + public OptionsBuilder withMaintainAspectRatio(final Boolean maintainAspectRatio) + { + this.maintainAspectRatio = maintainAspectRatio; + return this; + } + + public OptionsBuilder withCommandOptions(final String commandOptions) + { + this.commandOptions = commandOptions; + return this; + } + + public String build() + { + if (cropGravity != null) + { + cropGravity = cropGravity.trim(); + if (cropGravity.isEmpty()) + { + cropGravity = null; + } + else if (!GRAVITY_VALUES.contains(cropGravity)) + { + throw new TransformException(BAD_REQUEST.value(), "Invalid cropGravity value"); + } + } + + StringJoiner args = new StringJoiner(" "); + if (alphaRemove != null && alphaRemove) + { + args.add("-alpha"); + args.add(("remove")); + } + if (autoOrient != null && autoOrient) + { + args.add("-auto-orient"); + } + + if (cropGravity != null || cropWidth != null || cropHeight != null || cropPercentage != null || + cropXOffset != null || cropYOffset != null) + { + if (cropGravity != null) + { + args.add("-gravity"); + args.add(cropGravity); + } + + StringBuilder crop = new StringBuilder(); + if (cropWidth != null && cropWidth >= 0) + { + crop.append(cropWidth); + } + if (cropHeight != null && cropHeight >= 0) + { + crop.append('x'); + crop.append(cropHeight); + } + if (cropPercentage != null && cropPercentage) + { + crop.append('%'); + } + if (cropXOffset != null) + { + if (cropXOffset >= 0) + { + crop.append('+'); + } + crop.append(cropXOffset); + } + if (cropYOffset != null) + { + if (cropYOffset >= 0) + { + crop.append('+'); + } + crop.append(cropYOffset); + } + if (crop.length() > 1) + { + args.add("-crop"); + args.add(crop); + } + + args.add("+repage"); + } + + if (resizeHeight != null || resizeWidth != null || resizePercentage != null || maintainAspectRatio != null) + { + args.add(thumbnail != null && thumbnail ? "-thumbnail" : "-resize"); + StringBuilder resize = new StringBuilder(); + if (resizeWidth != null && resizeWidth >= 0) + { + resize.append(resizeWidth); + } + if (resizeHeight != null && resizeHeight >= 0) + { + resize.append('x'); + resize.append(resizeHeight); + } + if (resizePercentage != null && resizePercentage) + { + resize.append('%'); + } + if (allowEnlargement == null || !allowEnlargement) + { + resize.append('>'); + } + if (maintainAspectRatio != null && maintainAspectRatio) + { + resize.append('!'); + } + if (resize.length() > 1) + { + args.add(resize); + } + } + + return (commandOptions == null || "".equals( + commandOptions.trim()) ? "" : commandOptions + ' ') + + args.toString(); + } + + public static OptionsBuilder builder() + { + return new OptionsBuilder(); + } +} 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..9a276df1 --- /dev/null +++ b/alfresco-docker-imagemagick/src/main/java/org/alfresco/transformer/executors/ImageMagickCommandExecutor.java @@ -0,0 +1,54 @@ +package org.alfresco.transformer.executors; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.util.exec.RuntimeExec; +import org.springframework.stereotype.Component; + +/** + * CommandExecutor implementation for running ImageMagick transformations. It runs the + * transformation logic as a separate Shell process. + */ +@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..80ee9779 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,50 @@ */ 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.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.OK; 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 +78,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,11 +184,11 @@ 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)) - .andExpect(status().is(200)) + .andExpect(status().is(OK.value())) .andExpect(content().bytes(expectedTargetFileBytes)) .andExpect(header().string("Content-Disposition", "attachment; filename*= UTF-8''quick."+targetExtension)); } @@ -80,11 +197,11 @@ 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")) - .andExpect(status().is(400)); + .andExpect(status().is(BAD_REQUEST.value())); } @Test @@ -92,7 +209,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) @@ -116,7 +233,7 @@ public class ImageMagickControllerTest extends AbstractTransformerControllerTest .param("allowEnlargement", "true") .param("maintainAspectRatio", "true")) - .andExpect(status().is(200)) + .andExpect(status().is(OK.value())) .andExpect(content().bytes(expectedTargetFileBytes)) .andExpect(header().string("Content-Disposition", "attachment; filename*= UTF-8''quick."+targetExtension)); } @@ -126,7 +243,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) @@ -150,7 +267,7 @@ public class ImageMagickControllerTest extends AbstractTransformerControllerTest .param("allowEnlargement", "false") .param("maintainAspectRatio", "false")) - .andExpect(status().is(200)) + .andExpect(status().is(OK.value())) .andExpect(content().bytes(expectedTargetFileBytes)) .andExpect(header().string("Content-Disposition", "attachment; filename*= UTF-8''quick."+targetExtension)); } @@ -160,14 +277,14 @@ 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") .param("resizeWidth", "321") .param("resizeHeight", "654") .param("commandOptions", "( horrible command / );")) - .andExpect(status().is(200)) + .andExpect(status().is(OK.value())) .andExpect(content().bytes(expectedTargetFileBytes)) .andExpect(header().string("Content-Disposition", "attachment; filename*= UTF-8''quick."+targetExtension)); } @@ -180,4 +297,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(BAD_REQUEST.value())) + .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, 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..bb1fd260 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,7 @@ package org.alfresco.transformer; import io.micrometer.core.instrument.MeterRegistry; + import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; @@ -27,10 +28,11 @@ public class Application @Value("${container.name}") private String containerName; - @Bean MeterRegistryCustomizer metricsCommonTags() { + @Bean + MeterRegistryCustomizer metricsCommonTags() { return registry -> registry.config().commonTags("containerName", containerName); } - + public static void main(String[] args) { SpringApplication.run(Application.class, 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..872efe47 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,24 @@ */ 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.springframework.http.HttpStatus.OK; + 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 +38,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 +62,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 +112,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(OK.value(), "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 88% 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..4bc63ca2 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,25 +23,27 @@ * along with Alfresco. If not, see . * #L% */ -package org.alfresco.transformer; +package org.alfresco.transformer.executors; + +import static java.util.Arrays.asList; +import static java.util.Objects.requireNonNull; import java.io.File; -import java.io.FileFilter; -import java.io.FilenameFilter; import java.util.ArrayList; import java.util.List; import java.util.StringTokenizer; +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + import org.alfresco.error.AlfrescoRuntimeException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.artofsolving.jodconverter.office.DefaultOfficeManagerConfiguration; import org.artofsolving.jodconverter.office.OfficeException; 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 @@ -49,12 +51,12 @@ import org.springframework.beans.factory.InitializingBean; * * @author Neil McErlean */ -public class JodConverterSharedInstance implements InitializingBean, DisposableBean, JodConverter +public class JodConverterSharedInstance implements 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 +84,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 +98,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 +108,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 +149,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 +171,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 +209,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 +245,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 +253,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 +262,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 +272,8 @@ public class JodConverterSharedInstance implements InitializingBean, DisposableB { logger.debug("Cannot parse numerical value from " + string); } - // else intentionally empty } - return result; + return null; } /* @@ -283,14 +282,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() */ + @PostConstruct public void afterPropertiesSet() { // isAvailable defaults to false afterPropertiesSet. It only becomes true on successful completion of this method. @@ -318,7 +317,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 +417,7 @@ public class JodConverterSharedInstance implements InitializingBean, DisposableB private void logAllSofficeFilesUnderOfficeHome() { - if (logger.isDebugEnabled() == false) + if (!logger.isDebugEnabled()) { return; } @@ -430,7 +429,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 +448,10 @@ 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")); + results.addAll(asList(matchingFiles)); - for (File dir : searchRoot.listFiles(new FileFilter() - { - @Override - public boolean accept(File f) { - return f.isDirectory(); - } - })) + for (File dir : requireNonNull(searchRoot.listFiles(File::isDirectory))) { findSofficePrograms(dir, results, currentRecursionDepth + 1, maxRecursionDepth); } @@ -482,7 +465,7 @@ public class JodConverterSharedInstance implements InitializingBean, DisposableB */ private void logFileInfo(File f) { - if (logger.isDebugEnabled() == false) + if (!logger.isDebugEnabled()) { return; } @@ -512,7 +495,9 @@ public class JodConverterSharedInstance implements InitializingBean, DisposableB * (non-Javadoc) * @see org.springframework.beans.factory.DisposableBean#destroy() */ - public void destroy() throws Exception { + @PreDestroy + public void destroy() + { this.isAvailable = false; if (officeManager != null) { @@ -530,6 +515,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..311fc23f --- /dev/null +++ b/alfresco-docker-libreoffice/src/main/java/org/alfresco/transformer/executors/LibreOfficeJavaExecutor.java @@ -0,0 +1,136 @@ +package org.alfresco.transformer.executors; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; + +import java.io.File; +import java.io.IOException; + +import javax.annotation.PostConstruct; + +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; + +/** + * JavaExecutor implementation for running LibreOffice transformations. It loads the + * transformation logic in the same JVM (check the {@link JodConverter} implementation). + */ +@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; + + @PostConstruct + public void init() + { + 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(timeout); // 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(BAD_REQUEST.value(), + "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(INTERNAL_SERVER_ERROR.value(), + "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(INTERNAL_SERVER_ERROR.value(), + "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..af00d497 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,16 @@ */ package org.alfresco.transformer; -import io.micrometer.core.instrument.MeterRegistry; 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,7 +28,9 @@ public class Application @Value("${container.name}") private String containerName; - @Bean MeterRegistryCustomizer metricsCommonTags() { + @Bean + MeterRegistryCustomizer metricsCommonTags() + { return registry -> registry.config().commonTags("containerName", containerName); } 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..f4694b5b 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,33 @@ 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 static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.OK; 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,102 +47,95 @@ 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)) { - throw new TransformException(400, "Invalid transform value"); + throw new TransformException(BAD_REQUEST.value(), "Invalid transform value"); } 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 +143,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(OK.value(), "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 +167,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..7a02d79e --- /dev/null +++ b/alfresco-docker-tika/src/main/java/org/alfresco/transformer/executors/TikaJavaExecutor.java @@ -0,0 +1,99 @@ +package org.alfresco.transformer.executors; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; + +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.springframework.stereotype.Component; +import org.xml.sax.SAXException; + +/** + * JavaExecutor implementation for running TIKA transformations. It loads the + * transformation logic in the same JVM (check {@link Tika}). + */ +@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(BAD_REQUEST.value(), getMessage(e)); + } + catch (Exception e) + { + throw new TransformException(INTERNAL_SERVER_ERROR.value(), getMessage(e)); + } + if (!targetFile.exists() || targetFile.length() == 0) + { + throw new TransformException(INTERNAL_SERVER_ERROR.value(), + "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..d38fdc36 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,70 @@ 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.http.HttpStatus.INTERNAL_SERVER_ERROR; +import static org.springframework.http.HttpStatus.OK; 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 +110,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; @@ -120,7 +238,7 @@ public class TikaControllerTest extends AbstractTransformerControllerTest ? mockMvcRequest("/transform", sourceFile, "targetExtension", this.targetExtension) : mockMvcRequest("/transform", sourceFile, "targetExtension", this.targetExtension, "includeContents", includeContents.toString()); MvcResult result = mockMvc.perform(requestBuilder) - .andExpect(status().is(200)) + .andExpect(status().is(OK.value())) .andExpect(header().string("Content-Disposition", "attachment; filename*= UTF-8''quick." + this.targetExtension)). andReturn(); String content = result.getResponse().getContentAsString(); @@ -141,7 +259,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 +267,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 +285,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 +293,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 +301,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 +309,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 +317,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 +325,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,10 +334,10 @@ 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)); + .andExpect(status().is(INTERNAL_SERVER_ERROR.value())); } // --- Archive --- @@ -388,9 +498,9 @@ 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(status().is(OK.value())) .andExpect(header().string("Content-Disposition", "attachment; filename*= UTF-8''quick." + targetExtension)); } @@ -405,4 +515,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, 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..556a35d6 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,40 @@ */ 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.HttpStatus.INTERNAL_SERVER_ERROR; 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 +88,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 +98,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(INTERNAL_SERVER_ERROR.value()); + 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(INTERNAL_SERVER_ERROR.value()); + 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 +192,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(INTERNAL_SERVER_ERROR.value()); + 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 +257,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..b744ba4e --- /dev/null +++ b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/TransformController.java @@ -0,0 +1,145 @@ +package org.alfresco.transformer; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; + +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; + +/** + * TransformController interface. + *
+ * It contains much of the common boilerplate code that each of + * its concrete implementations need as default methods. + */ +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 = BAD_REQUEST.value(); + + 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 = BAD_REQUEST.value(); + + 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..8b80446f 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,26 @@ */ 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; +/** + * TransformInterceptor + *
+ * Handles ThreadLocal Log entries for each request. + */ 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 +53,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..ee54490b --- /dev/null +++ b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/executors/AbstractCommandExecutor.java @@ -0,0 +1,74 @@ +package org.alfresco.transformer.executors; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; + +import java.io.File; +import java.util.Map; + +import org.alfresco.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(BAD_REQUEST.value(), + "Transformer exit code was not 0: \n" + result.getStdErr()); + } + + if (!targetFile.exists() || targetFile.length() == 0) + { + throw new TransformException(INTERNAL_SERVER_ERROR.value(), + "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(INTERNAL_SERVER_ERROR.value(), + "Transformer version check exit code was not 0: \n" + result); + } + + version = result.getStdOut().trim(); + if (version.isEmpty()) + { + throw new TransformException(INTERNAL_SERVER_ERROR.value(), + "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..e983733f --- /dev/null +++ b/alfresco-transformer-base/src/main/java/org/alfresco/transformer/fs/FileManager.java @@ -0,0 +1,202 @@ +package org.alfresco.transformer.fs; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.INSUFFICIENT_STORAGE; +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; + +import 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 ? BAD_REQUEST.value() : INTERNAL_SERVER_ERROR.value(); + 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(INSUFFICIENT_STORAGE.value(), + "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(INSUFFICIENT_STORAGE.value(), "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(INTERNAL_SERVER_ERROR.value(), + "Could not read the target file: " + file.getPath()); + } + } + catch (MalformedURLException e) + { + throw new TransformException(INTERNAL_SERVER_ERROR.value(), + "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 92% 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..28839cc0 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,10 @@ * along with Alfresco. If not, see . * #L% */ -package org.alfresco.transformer; +package org.alfresco.transformer.logging; + +import static java.lang.Math.max; +import static org.springframework.http.HttpStatus.OK; import java.text.SimpleDateFormat; import java.util.Collection; @@ -32,7 +35,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 +44,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 +54,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(); @@ -197,16 +197,16 @@ public class LogEntry public static void complete() { LogEntry logEntry = currentLogEntry.get(); - if (logEntry.statusCode == 200) + if (logEntry.statusCode == OK.value()) { logEntry.durationStreamOut = System.currentTimeMillis() - logEntry.start - logEntry.durationStreamIn - max(logEntry.durationTransform, 0) - max(logEntry.durationDelay, 0); } currentLogEntry.remove(); - if (AbstractTransformerController.logger != null && AbstractTransformerController.logger.isDebugEnabled()) + if (logger.isDebugEnabled()) { - AbstractTransformerController.logger.debug(logEntry.toString()); + logger.debug(logEntry.toString()); } } @@ -279,6 +279,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 77% 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..bc0a9442 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,14 @@ * 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 static org.springframework.http.HttpStatus.INSUFFICIENT_STORAGE; +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; +import static org.springframework.http.HttpStatus.OK; +import static org.springframework.http.HttpStatus.TOO_MANY_REQUESTS; import java.io.File; import java.io.IOException; @@ -35,6 +42,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 +70,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 +80,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 +117,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 +148,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 +191,7 @@ abstract class ProbeTestTransform { String probeMessage = getProbeMessage(isLiveProbe); String message = "Success - No transform."; - LogEntry.setStatusCodeAndMessage(200, probeMessage+message); + LogEntry.setStatusCodeAndMessage(OK.value(), probeMessage + message); if (!isLiveProbe && !readySent.getAndSet(true)) { logger.info(probeMessage+message); @@ -179,7 +199,7 @@ abstract class ProbeTestTransform return message; } - String doTransform(HttpServletRequest request, boolean isLiveProbe) + private String doTransform(HttpServletRequest request, boolean isLiveProbe) { checkMaxTransformTimeAndCount(isLiveProbe); @@ -207,9 +227,10 @@ 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(INTERNAL_SERVER_ERROR.value(), + 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. @@ -224,31 +245,31 @@ abstract class ProbeTestTransform { if (die.get()) { - throw new TransformException(429, getMessagePrefix(isLiveProbe) + + throw new TransformException(TOO_MANY_REQUESTS.value(), getMessagePrefix(isLiveProbe) + "Transformer requested to die. A transform took longer than "+ (maxTransformTime *1000)+" seconds"); } if (maxTransformCount > 0 && transformCount.get() > maxTransformCount) { - throw new TransformException(429, getMessagePrefix(isLiveProbe) + + throw new TransformException(TOO_MANY_REQUESTS.value(), getMessagePrefix(isLiveProbe) + "Transformer requested to die. It has performed more than "+ maxTransformCount+" transformations"); } } - 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); } catch (IOException e) { - throw new TransformException(507, getMessagePrefix(isLiveProbe)+ + throw new TransformException(INSUFFICIENT_STORAGE.value(), getMessagePrefix(isLiveProbe)+ "Failed to store the source file", e); } long length = sourceFile.length(); @@ -256,15 +277,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 +293,7 @@ abstract class ProbeTestTransform } } - void calculateMaxTime(long time, boolean isLiveProbe) + public void calculateMaxTime(long time, boolean isLiveProbe) { if (transCount <= AVERAGE_OVER_TRANSFORMS) { @@ -304,17 +325,19 @@ abstract class ProbeTestTransform String probeMessage = getProbeMessage(isLiveProbe); if (!targetFile.exists() || !targetFile.isFile()) { - throw new TransformException(500, probeMessage +"Target File \""+targetFile.getAbsolutePath()+"\" did not exist"); + throw new TransformException(INTERNAL_SERVER_ERROR.value(), + probeMessage + "Target File \"" + targetFile.getAbsolutePath() + "\" did not exist"); } long length = targetFile.length(); if (length < minExpectedLength || length > maxExpectedLength) { - throw new TransformException(500, probeMessage +"Target File \""+targetFile.getAbsolutePath()+ - "\" was the wrong size ("+ length+"). Needed to be between "+minExpectedLength+" and "+ - maxExpectedLength); + throw new TransformException(INTERNAL_SERVER_ERROR.value(), + probeMessage + "Target File \"" + targetFile.getAbsolutePath() + + "\" was the wrong size (" + length + "). Needed to be between " + + minExpectedLength + " and " + maxExpectedLength); } LogEntry.setTargetSize(length); - LogEntry.setStatusCodeAndMessage(200, probeMessage +"Success - "+message); + LogEntry.setStatusCodeAndMessage(OK.value(), probeMessage + "Success - " + message); } private String getMessagePrefix(boolean isLiveProbe) @@ -331,4 +354,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..6de4388b 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,10 @@ 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.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; +import static org.springframework.http.HttpStatus.OK; 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 +42,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 +71,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 +85,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 +100,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 +127,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 +136,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(OK.value())) + .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(INTERNAL_SERVER_ERROR.value())); } @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(OK.value())) + .andExpect(content().bytes(expectedTargetFileBytes)) + .andExpect(header().string("Content-Disposition", "attachment; filename*= UTF-8''quick." + targetExtension)); } @Test @@ -313,9 +206,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(OK.value())) + .andExpect(content().bytes(expectedTargetFileBytes)) + .andExpect(header().string("Content-Disposition", "attachment; filename*= UTF-8''quick." + targetExtension)); } @Test @@ -325,8 +218,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(BAD_REQUEST.value())) + .andExpect(status().reason(containsString("The source filename was not supplied"))); } @Test @@ -335,113 +228,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(BAD_REQUEST.value())) + .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(BAD_REQUEST.value())) + .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,17 +277,18 @@ 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)) - .andExpect(status().is(HttpStatus.BAD_REQUEST.value())) + 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(BAD_REQUEST.value())) .andReturn().getResponse().getContentAsString(); TransformReply transformReply = objectMapper.readValue(transformationReplyAsString, TransformReply.class); // Assert the reply - assertEquals(HttpStatus.BAD_REQUEST.value(), transformReply.getStatus()); + assertEquals(BAD_REQUEST.value(), transformReply.getStatus()); } - - protected abstract void updateTransformRequestWithSpecificOptions(TransformRequest transformRequest); }