From e06989e5444cf2c0a91370460600b83ecd227c9d Mon Sep 17 00:00:00 2001 From: alandavis Date: Mon, 25 Jul 2022 17:20:13 +0100 Subject: [PATCH] Save point: [skip ci] * TikaTests --- .../test/resources/tika_engine_config.json | 2 +- .../transform/base/TransformHandler.java | 8 +- .../transform/base/TransformProcess.java | 2 +- .../AbstractMetadataExtractor.java | 2 + .../PoiMetadataEmbedder.java | 165 ++++++++++++++++++ .../PoiMetadataExtractor.java | 96 ---------- .../transform/tika/transformers/Tika.java | 10 +- .../org/alfresco/transform/tika/TikaTest.java | 40 +++-- .../transform/tika/TikaTransformationIT.java | 2 +- engines/tika/src/test/resources/quick.xslx | Bin 8643 -> 0 bytes .../test/resources/tika_engine_config.json | 2 +- .../transform/common/ExtensionService.java | 4 +- 12 files changed, 210 insertions(+), 123 deletions(-) create mode 100644 engines/tika/src/main/java/org/alfresco/transform/tika/metadataExtractors/PoiMetadataEmbedder.java delete mode 100644 engines/tika/src/test/resources/quick.xslx diff --git a/engines/aio/src/test/resources/tika_engine_config.json b/engines/aio/src/test/resources/tika_engine_config.json index b6d534b7..a038950c 100644 --- a/engines/aio/src/test/resources/tika_engine_config.json +++ b/engines/aio/src/test/resources/tika_engine_config.json @@ -987,7 +987,7 @@ ] }, { - "transformerName": "SamplePoiMetadataEmbedder", + "transformerName": "PoiMetadataEmbedder", "supportedSourceAndTargetList": [ {"sourceMediaType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "targetMediaType": "alfresco-metadata-embed"} ], diff --git a/engines/base/src/main/java/org/alfresco/transform/base/TransformHandler.java b/engines/base/src/main/java/org/alfresco/transform/base/TransformHandler.java index 104f2c4b..885db426 100644 --- a/engines/base/src/main/java/org/alfresco/transform/base/TransformHandler.java +++ b/engines/base/src/main/java/org/alfresco/transform/base/TransformHandler.java @@ -189,7 +189,7 @@ public class TransformHandler MultipartFile sourceMultipartFile, String sourceMimetype, String targetMimetype, Map requestParameters) { - return createResponseEntity(targetMimetype, os -> + return createResponseEntity(sourceMimetype, targetMimetype, os -> { new TransformProcess(this, sourceMimetype, targetMimetype, requestParameters, "e" + httpRequestCount.getAndIncrement()) @@ -216,7 +216,7 @@ public class TransformHandler @Override protected OutputStream getOutputStream() { - return transformManager.setOutputStream(os); + return os; } @Override @@ -538,10 +538,10 @@ public class TransformHandler return customTransformer; } - private ResponseEntity createResponseEntity(String targetMimetype, + private ResponseEntity createResponseEntity(String sourceMimetype, String targetMimetype, StreamingResponseBody body) { - String extension = ExtensionService.getExtensionForMimetype(targetMimetype); + String extension = ExtensionService.getExtensionForTargetMimetype(targetMimetype, sourceMimetype); HttpHeaders headers = new HttpHeaders(); headers.setContentDisposition( ContentDisposition.attachment() diff --git a/engines/base/src/main/java/org/alfresco/transform/base/TransformProcess.java b/engines/base/src/main/java/org/alfresco/transform/base/TransformProcess.java index 57736d39..9898d7cd 100644 --- a/engines/base/src/main/java/org/alfresco/transform/base/TransformProcess.java +++ b/engines/base/src/main/java/org/alfresco/transform/base/TransformProcess.java @@ -36,6 +36,7 @@ import org.springframework.web.multipart.MultipartFile; import javax.jms.Destination; import javax.servlet.http.HttpServletRequest; import java.io.File; +import java.io.OutputStream; import java.util.Map; import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; @@ -70,7 +71,6 @@ abstract class TransformProcess extends TransformStreamHandler transformHandler.getProbeTransform().incrementTransformerCount(); } - @Override public void handleTransformRequest() { transformManager.setSourceMimetype(sourceMimetype); diff --git a/engines/base/src/main/java/org/alfresco/transform/base/metadataExtractors/AbstractMetadataExtractor.java b/engines/base/src/main/java/org/alfresco/transform/base/metadataExtractors/AbstractMetadataExtractor.java index 7d765f92..10571fb6 100644 --- a/engines/base/src/main/java/org/alfresco/transform/base/metadataExtractors/AbstractMetadataExtractor.java +++ b/engines/base/src/main/java/org/alfresco/transform/base/metadataExtractors/AbstractMetadataExtractor.java @@ -374,6 +374,8 @@ public abstract class AbstractMetadataExtractor implements CustomTransformer String className = this.getClass().getName(); String shortClassName = className.split("\\.")[className.split("\\.").length - 1]; shortClassName = shortClassName.replace('$', '-'); + // The embedder uses the reverse of the extractor's data. + shortClassName = shortClassName.replace("Embedder", "Extractor"); return shortClassName + "_metadata_" + suffix + ".properties"; } diff --git a/engines/tika/src/main/java/org/alfresco/transform/tika/metadataExtractors/PoiMetadataEmbedder.java b/engines/tika/src/main/java/org/alfresco/transform/tika/metadataExtractors/PoiMetadataEmbedder.java new file mode 100644 index 00000000..8102547d --- /dev/null +++ b/engines/tika/src/main/java/org/alfresco/transform/tika/metadataExtractors/PoiMetadataEmbedder.java @@ -0,0 +1,165 @@ +/* + * #%L + * Alfresco Transform Core + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.transform.tika.metadataExtractors; + +import org.apache.poi.ooxml.POIXMLProperties; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.apache.tika.embedder.Embedder; +import org.apache.tika.metadata.Metadata; +import org.apache.tika.mime.MediaType; +import org.apache.tika.parser.ParseContext; +import org.apache.tika.parser.Parser; +import org.apache.tika.parser.microsoft.ooxml.OOXMLParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Collections; +import java.util.Set; +import java.util.StringJoiner; + +import static org.alfresco.transform.base.metadataExtractors.AbstractMetadataExtractor.Type.EXTRACTOR; + +/** + * Sample POI metadata embedder to demonstrate it is possible to add custom T-Engines that will add + * metadata. This is not production code, so no supported mimetypes exist in the {@code tika_engine_config.json}. + * Adding the following would make it available: + * + *
+ * {
+ *   "transformOptions": {
+ *     ...
+ *     "metadataEmbedOptions": [
+ *       {"value": {"name": "metadata", "required": true}}
+ *     ]
+ *   },
+ *   "transformers": [
+ *     ...
+ *     {
+ *       "transformerName": "PoiMetadataEmbedder",
+ *       "supportedSourceAndTargetList": [
+ *         ...
+ *         {"sourceMediaType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "targetMediaType": "alfresco-metadata-embed"}
+ *       ],
+ *       "transformOptions": [
+ *         "metadataEmbedOptions"
+ *       ]
+ *     }
+ *   ]
+ * }
+ * 
+ + * @author Nick Burch + * @author Neil McErlean + * @author Dmitry Velichkevich + * @author adavis + */ +@Component +public class PoiMetadataEmbedder extends AbstractTikaMetadataExtractor +{ + private static final Logger logger = LoggerFactory.getLogger(PoiMetadataEmbedder.class); + + public PoiMetadataEmbedder() + { + super(EXTRACTOR, logger); + } + + @Override + protected Parser getParser() + { + return new OOXMLParser(); + } + + @Override + protected Embedder getEmbedder() + { + return new SamplePoiEmbedder(); + } + + private static class SamplePoiEmbedder implements Embedder + { + private static final Set SUPPORTED_EMBED_TYPES = + Collections.singleton(MediaType.application("vnd.openxmlformats-officedocument.spreadsheetml.sheet")); + + @Override + public Set getSupportedEmbedTypes(ParseContext parseContext) + { + return SUPPORTED_EMBED_TYPES; + } + + @Override + public void embed(Metadata metadata, InputStream inputStream, OutputStream outputStream, ParseContext parseContext) + throws IOException + { + XSSFWorkbook workbook = new XSSFWorkbook(inputStream); + POIXMLProperties props = workbook.getProperties(); + + POIXMLProperties.CoreProperties coreProp = props.getCoreProperties(); + POIXMLProperties.CustomProperties custProp = props.getCustomProperties(); + + for (String name : metadata.names()) + { + metadata.isMultiValued("description"); + String value = null; + if (metadata.isMultiValued(name)) + { + String[] values = metadata.getValues(name); + StringJoiner sj = new StringJoiner(", "); + for (String s : values) + { + sj.add(s); + } + value = sj.toString(); + } + else + { + value = metadata.get(name); + } + switch (name) + { + case "author": + coreProp.setCreator(value); + break; + case "title": + coreProp.setTitle(value); + break; + case "description": + coreProp.setDescription(value); + break; + // There are other core values but this is sample code, so we will assume it is a custom value. + default: + custProp.addProperty(name, value); + break; + } + } + workbook.write(outputStream); + } + } +} diff --git a/engines/tika/src/main/java/org/alfresco/transform/tika/metadataExtractors/PoiMetadataExtractor.java b/engines/tika/src/main/java/org/alfresco/transform/tika/metadataExtractors/PoiMetadataExtractor.java index c110c3d5..9872ee4e 100644 --- a/engines/tika/src/main/java/org/alfresco/transform/tika/metadataExtractors/PoiMetadataExtractor.java +++ b/engines/tika/src/main/java/org/alfresco/transform/tika/metadataExtractors/PoiMetadataExtractor.java @@ -59,36 +59,6 @@ import static org.alfresco.transform.base.metadataExtractors.AbstractMetadataExt * created: -- cm:created * Any custom property: -- [not mapped] * - * - * Uses Apache Tika - * - * Also includes a sample POI metadata embedder to demonstrate it is possible to add custom T-Engines that will add - * metadata. This is not production code so no supported mimetypes exist in the {@code tika_engine_config.json}. - * Adding the following would make it available: - * - *
- * {
- *   "transformOptions": {
- *     ...
- *     "metadataEmbedOptions": [
- *       {"value": {"name": "metadata", "required": true}}
- *     ]
- *   },
- *   "transformers": [
- *     ...
- *     {
- *       "transformerName": "SamplePoiMetadataEmbedder",
- *       "supportedSourceAndTargetList": [
- *         ...
- *         {"sourceMediaType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "targetMediaType": "alfresco-metadata-embed"}
- *       ],
- *       "transformOptions": [
- *         "metadataEmbedOptions"
- *       ]
- *     }
- *   ]
- * }
- * 
* @author Nick Burch * @author Neil McErlean @@ -110,70 +80,4 @@ public class PoiMetadataExtractor extends AbstractTikaMetadataExtractor { return new OOXMLParser(); } - - @Override - protected Embedder getEmbedder() - { - return new SamplePoiEmbedder(); - } - - private static class SamplePoiEmbedder implements Embedder - { - private static final Set SUPPORTED_EMBED_TYPES = - Collections.singleton(MediaType.application("vnd.openxmlformats-officedocument.spreadsheetml.sheet")); - - @Override - public Set getSupportedEmbedTypes(ParseContext parseContext) - { - return SUPPORTED_EMBED_TYPES; - } - - @Override - public void embed(Metadata metadata, InputStream inputStream, OutputStream outputStream, ParseContext parseContext) - throws IOException - { - XSSFWorkbook workbook = new XSSFWorkbook(inputStream); - POIXMLProperties props = workbook.getProperties(); - - POIXMLProperties.CoreProperties coreProp = props.getCoreProperties(); - POIXMLProperties.CustomProperties custProp = props.getCustomProperties(); - - for (String name : metadata.names()) - { - metadata.isMultiValued("description"); - String value = null; - if (metadata.isMultiValued(name)) - { - String[] values = metadata.getValues(name); - StringJoiner sj = new StringJoiner(", "); - for (String s : values) - { - sj.add(s); - } - value = sj.toString(); - } - else - { - value = metadata.get(name); - } - switch (name) - { - case "author": - coreProp.setCreator(value); - break; - case "title": - coreProp.setTitle(value); - break; - case "description": - coreProp.setDescription(value); - break; - // There are other core values but this is sample code, so we will assume it is a custom value. - default: - custProp.addProperty(name, value); - break; - } - } - workbook.write(outputStream); - } - } } diff --git a/engines/tika/src/main/java/org/alfresco/transform/tika/transformers/Tika.java b/engines/tika/src/main/java/org/alfresco/transform/tika/transformers/Tika.java index 2fa22ad6..ef2459f1 100644 --- a/engines/tika/src/main/java/org/alfresco/transform/tika/transformers/Tika.java +++ b/engines/tika/src/main/java/org/alfresco/transform/tika/transformers/Tika.java @@ -54,14 +54,12 @@ 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.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.UnsupportedEncodingException; import java.io.Writer; import java.net.URL; import java.util.List; @@ -103,7 +101,7 @@ public class Tika public static final String PPTX = "pptx"; public static final String TXT = "txt"; public static final String XHTML = "xhtml"; - public static final String XSLX = "xslx"; + public static final String XLSX = "xlsx"; public static final String XML = "xml"; public static final String ZIP = "zip"; @@ -236,6 +234,10 @@ public class Tika parser.parse(inputStream, handler, metadata, context); } + catch (UnsupportedEncodingException e) + { + throw new IllegalStateException("Unsupported encoding "+e.getMessage(), e); + } catch (SAXException | TikaException | IOException e) { throw new IllegalStateException(e.getMessage(), e); diff --git a/engines/tika/src/test/java/org/alfresco/transform/tika/TikaTest.java b/engines/tika/src/test/java/org/alfresco/transform/tika/TikaTest.java index fe05c669..b31b441f 100644 --- a/engines/tika/src/test/java/org/alfresco/transform/tika/TikaTest.java +++ b/engines/tika/src/test/java/org/alfresco/transform/tika/TikaTest.java @@ -34,11 +34,9 @@ import org.alfresco.transform.client.model.TransformReply; import org.alfresco.transform.client.model.TransformRequest; import org.apache.poi.ooxml.POIXMLProperties; import org.apache.poi.xssf.usermodel.XSSFWorkbook; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; -import org.mockito.stubbing.Answer; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; @@ -53,10 +51,8 @@ import javax.servlet.http.HttpServletRequest; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; -import java.util.Map; import java.util.UUID; -import static java.nio.file.Files.readAllBytes; import static org.alfresco.transform.common.Mimetype.MIMETYPE_HTML; import static org.alfresco.transform.common.Mimetype.MIMETYPE_METADATA_EMBED; import static org.alfresco.transform.common.Mimetype.MIMETYPE_OPENXML_PRESENTATION; @@ -91,14 +87,13 @@ import static org.alfresco.transform.tika.transformers.Tika.TIKA_AUTO; import static org.alfresco.transform.tika.transformers.Tika.TXT; import static org.alfresco.transform.tika.transformers.Tika.XHTML; import static org.alfresco.transform.tika.transformers.Tika.XML; -import static org.alfresco.transform.tika.transformers.Tika.XSLX; +import static org.alfresco.transform.tika.transformers.Tika.XLSX; import static org.alfresco.transform.tika.transformers.Tika.ZIP; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.when; import static org.springframework.http.HttpHeaders.ACCEPT; import static org.springframework.http.HttpHeaders.CONTENT_DISPOSITION; @@ -109,7 +104,8 @@ import static org.springframework.http.HttpStatus.OK; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.http.MediaType.APPLICATION_PDF_VALUE; import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE; -import static org.springframework.util.StringUtils.getFilenameExtension; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; /** * Test Tika. @@ -179,7 +175,11 @@ public class TikaTest extends AbstractBaseTest "targetExtension", this.targetExtension) : mockMvcRequest(ENDPOINT_TRANSFORM, sourceFile, "targetExtension", this.targetExtension, INCLUDE_CONTENTS, includeContents.toString()); - MvcResult result = mockMvc.perform(requestBuilder) + MvcResult mvcResult = mockMvc.perform(requestBuilder) + .andExpect(request().asyncStarted()) + .andReturn(); + + MvcResult result = mockMvc.perform(asyncDispatch(mvcResult)) .andExpect(MockMvcResultMatchers.status().is(OK.value())) .andExpect(MockMvcResultMatchers.header().string("Content-Disposition", "attachment; filename*=UTF-8''transform." + this.targetExtension)). @@ -252,9 +252,17 @@ public class TikaTest extends AbstractBaseTest { mockTransformCommand(PDF, TXT, MIMETYPE_PDF, true); targetEncoding = "rubbish"; - mockMvc.perform( +// mockMvc.perform( +// mockMvcRequest(ENDPOINT_TRANSFORM, sourceFile, "targetExtension", targetExtension)) +// .andExpect(MockMvcResultMatchers.status().is(INTERNAL_SERVER_ERROR.value())); + + MvcResult mvcResult = mockMvc.perform( mockMvcRequest(ENDPOINT_TRANSFORM, sourceFile, "targetExtension", targetExtension)) - .andExpect(MockMvcResultMatchers.status().is(INTERNAL_SERVER_ERROR.value())); + .andExpect(request().asyncStarted()) + .andReturn(); + + mockMvc.perform(asyncDispatch(mvcResult)) + .andExpect(MockMvcResultMatchers.status().is(INTERNAL_SERVER_ERROR.value())); } // --- Archive --- @@ -381,7 +389,7 @@ public class TikaTest extends AbstractBaseTest @Test public void xslxToCsvPoiTest() throws Exception { - transform(POI, XSLX, CSV, MIMETYPE_OPENXML_SPREADSHEET, MIMETYPE_TEXT_CSV, null, + transform(POI, XLSX, CSV, MIMETYPE_OPENXML_SPREADSHEET, MIMETYPE_TEXT_CSV, null, EXPECTED_CSV_CONTENT_CONTAINS); } @@ -429,7 +437,7 @@ public class TikaTest extends AbstractBaseTest @Test public void xlsxEmbedTest() throws Exception { - mockTransformCommand(XSLX, XSLX, MIMETYPE_OPENXML_SPREADSHEET, false); + mockTransformCommand(XLSX, XLSX, MIMETYPE_OPENXML_SPREADSHEET, false); String metadata = "{\"{http://www.alfresco.org/model/content/1.0}author\":\"author1\"," + @@ -439,12 +447,16 @@ public class TikaTest extends AbstractBaseTest MockHttpServletRequestBuilder requestBuilder = super.mockMvcRequest(ENDPOINT_TRANSFORM, sourceFile, - "targetExtension", XSLX, + "targetExtension", XLSX, "metadata", metadata, "targetMimetype", MIMETYPE_METADATA_EMBED, "sourceMimetype", MIMETYPE_OPENXML_SPREADSHEET); - MvcResult result = mockMvc.perform(requestBuilder) + MvcResult mvcResult = mockMvc.perform(requestBuilder) + .andExpect(request().asyncStarted()) + .andReturn(); + + MvcResult result = mockMvc.perform(asyncDispatch(mvcResult)) .andExpect(MockMvcResultMatchers.status().is(OK.value())) .andExpect(MockMvcResultMatchers.header().string("Content-Disposition", "attachment; filename*=UTF-8''transform." + targetExtension)). diff --git a/engines/tika/src/test/java/org/alfresco/transform/tika/TikaTransformationIT.java b/engines/tika/src/test/java/org/alfresco/transform/tika/TikaTransformationIT.java index dfbbc29d..10153ecb 100644 --- a/engines/tika/src/test/java/org/alfresco/transform/tika/TikaTransformationIT.java +++ b/engines/tika/src/test/java/org/alfresco/transform/tika/TikaTransformationIT.java @@ -152,7 +152,7 @@ public class TikaTransformationIT allTargets("quick.txt", "text/plain"), allTargets("quick.vsd", "application/vnd.visio"), allTargets("quick.xls", "application/vnd.ms-excel"), - allTargets("quick.xslx", + allTargets("quick.xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"), allTargets("quick.zip", "application/zip"), allTargets("quick.tar", "application/x-tar"), diff --git a/engines/tika/src/test/resources/quick.xslx b/engines/tika/src/test/resources/quick.xslx deleted file mode 100644 index 2e1f271ed89954532bba77af998d8ed25f4abf69..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8643 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