diff --git a/build.gradle b/build.gradle index 9750a91a93..4cef662f0d 100644 --- a/build.gradle +++ b/build.gradle @@ -87,8 +87,8 @@ ext { lettuceVersion = '6.2.3.RELEASE' log4jVersion = '2.19.0' mailVersion = '1.0.0' - micrometerPropagationVersion = '1.0.1-SNAPSHOT' - micrometerTracingVersion = '1.0.2-SNAPSHOT' + micrometerPropagationVersion = '1.0.2' + micrometerTracingVersion = '1.0.2' micrometerVersion = '1.11.0-SNAPSHOT' mockitoVersion = '5.1.1' mongoDriverVersion = '4.8.2' @@ -96,7 +96,7 @@ ext { pahoMqttClientVersion = '1.2.5' postgresVersion = '42.5.2' r2dbch2Version = '1.0.0.RELEASE' - reactorVersion = '2022.0.3-SNAPSHOT' + reactorVersion = '2022.0.3' resilience4jVersion = '2.0.2' romeToolsVersion = '1.18.0' rsocketVersion = '1.1.3' @@ -104,16 +104,17 @@ ext { smackVersion = '4.4.6' springAmqpVersion = '3.0.2-SNAPSHOT' springDataVersion = '2023.0.0-SNAPSHOT' - springGraphqlVersion = '1.2.0-SNAPSHOT' + springGraphqlVersion = '1.1.2-SNAPSHOT' springKafkaVersion = '3.0.3-SNAPSHOT' springRetryVersion = '2.0.0' springSecurityVersion = '6.1.0-SNAPSHOT' - springVersion = '6.0.5-SNAPSHOT' + springVersion = '6.0.5' springWsVersion = '4.0.1' testcontainersVersion = '1.17.6' tomcatVersion = '11.0.0-M1' xmlUnitVersion = '2.9.1' xstreamVersion = '1.4.20' + ztZipVersion = '1.15' javaProjects = subprojects - project(':spring-integration-bom') } @@ -1094,6 +1095,15 @@ project('spring-integration-zeromq') { } } +project('spring-integration-zip') { + description = 'Spring Integration Zip Support' + dependencies { + api project(':spring-integration-file') + api "org.zeroturnaround:zt-zip:$ztZipVersion" + } +} + + project('spring-integration-zookeeper') { description = 'Spring Integration Zookeeper Support' dependencies { diff --git a/spring-integration-zip/src/main/java/org/springframework/integration/zip/ZipHeaders.java b/spring-integration-zip/src/main/java/org/springframework/integration/zip/ZipHeaders.java new file mode 100644 index 0000000000..287a02e1a0 --- /dev/null +++ b/spring-integration-zip/src/main/java/org/springframework/integration/zip/ZipHeaders.java @@ -0,0 +1,36 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.zip; + +/** + * Zip adapter specific message headers. + * + * @author Gunnar Hillert + * + * @since 6.1 + */ +public abstract class ZipHeaders { + + public static final String PREFIX = "zip_"; + + public static final String ZIP_ENTRY_FILE_NAME = PREFIX + "entryFilename"; + + public static final String ZIP_ENTRY_PATH = PREFIX + "entryPath"; + + public static final String ZIP_ENTRY_LAST_MODIFIED_DATE = PREFIX + "entryLastModifiedDate"; + +} diff --git a/spring-integration-zip/src/main/java/org/springframework/integration/zip/config/xml/AbstractZipTransformerParser.java b/spring-integration-zip/src/main/java/org/springframework/integration/zip/config/xml/AbstractZipTransformerParser.java new file mode 100644 index 0000000000..dea1dff980 --- /dev/null +++ b/spring-integration-zip/src/main/java/org/springframework/integration/zip/config/xml/AbstractZipTransformerParser.java @@ -0,0 +1,63 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.zip.config.xml; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.integration.config.xml.AbstractTransformerParser; +import org.springframework.integration.config.xml.IntegrationNamespaceUtils; +import org.springframework.util.StringUtils; + +/** + * Base class for Zip transformer parsers. + * + * @author Gunnar Hillert + * @author Artem Bilan + * + * @since 6.1 + */ +public abstract class AbstractZipTransformerParser extends AbstractTransformerParser { + + /** + * @param element The XML Element to process + * @param parserContext The Spring ParserContext + * @param builder BeanDefinitionBuilder for constructing Bean Definitions + */ + @Override + protected final void parseTransformer(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + String deleteFiles = element.getAttribute("delete-files"); + if (StringUtils.hasText(deleteFiles)) { + builder.addPropertyValue("deleteFiles", deleteFiles); + } + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "charset"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "result-type", "zipResultType"); + postProcessTransformer(element, parserContext, builder); + } + + /** + * Subclasses may override this method to provide additional configuration. + * + * @param element The XML Element to process + * @param parserContext The Spring ParserContext + * @param builder BeanDefinitionBuilder for constructing Bean Definitions + */ + protected void postProcessTransformer(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + } + +} diff --git a/spring-integration-zip/src/main/java/org/springframework/integration/zip/config/xml/UnZipTransformerParser.java b/spring-integration-zip/src/main/java/org/springframework/integration/zip/config/xml/UnZipTransformerParser.java new file mode 100644 index 0000000000..eaf82182db --- /dev/null +++ b/spring-integration-zip/src/main/java/org/springframework/integration/zip/config/xml/UnZipTransformerParser.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.zip.config.xml; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.integration.config.xml.IntegrationNamespaceUtils; +import org.springframework.integration.zip.transformer.UnZipTransformer; + +/** + * Parser for the 'unzip-transformer' element. + * + * @author Gunnar Hillert + * @author Artem Bilan + * + * @since 6.1 + */ +public class UnZipTransformerParser extends AbstractZipTransformerParser { + + @Override + protected String getTransformerClassName() { + return UnZipTransformer.class.getName(); + } + + @Override + protected void postProcessTransformer(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "expect-single-result"); + } + +} diff --git a/spring-integration-zip/src/main/java/org/springframework/integration/zip/config/xml/ZipNamespaceHandler.java b/spring-integration-zip/src/main/java/org/springframework/integration/zip/config/xml/ZipNamespaceHandler.java new file mode 100644 index 0000000000..5643ece885 --- /dev/null +++ b/spring-integration-zip/src/main/java/org/springframework/integration/zip/config/xml/ZipNamespaceHandler.java @@ -0,0 +1,37 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.zip.config.xml; + +import org.springframework.integration.config.xml.AbstractIntegrationNamespaceHandler; + +/** + * The namespace handler for the Zip namespace + * + * @author Gunnar Hillert + * + * @since 6.1 + * + */ +public class ZipNamespaceHandler extends AbstractIntegrationNamespaceHandler { + + @Override + public void init() { + this.registerBeanDefinitionParser("zip-transformer", new ZipTransformerParser()); + this.registerBeanDefinitionParser("unzip-transformer", new UnZipTransformerParser()); + } + +} diff --git a/spring-integration-zip/src/main/java/org/springframework/integration/zip/config/xml/ZipTransformerParser.java b/spring-integration-zip/src/main/java/org/springframework/integration/zip/config/xml/ZipTransformerParser.java new file mode 100644 index 0000000000..344bcbb165 --- /dev/null +++ b/spring-integration-zip/src/main/java/org/springframework/integration/zip/config/xml/ZipTransformerParser.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.zip.config.xml; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.integration.config.xml.IntegrationNamespaceUtils; +import org.springframework.integration.zip.transformer.ZipTransformer; + +/** + * Parser for the 'zip-transformer' element. + * + * @author Gunnar Hillert + * @author Artem Bilan + * + * @since 6.1 + */ +public class ZipTransformerParser extends AbstractZipTransformerParser { + + @Override + protected String getTransformerClassName() { + return ZipTransformer.class.getName(); + } + + @Override + protected void postProcessTransformer(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "compression-level"); + } + +} diff --git a/spring-integration-zip/src/main/java/org/springframework/integration/zip/config/xml/package-info.java b/spring-integration-zip/src/main/java/org/springframework/integration/zip/config/xml/package-info.java new file mode 100644 index 0000000000..e23dd6fb94 --- /dev/null +++ b/spring-integration-zip/src/main/java/org/springframework/integration/zip/config/xml/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides parser classes to provide Xml namespace support for the Zip components. + */ +package org.springframework.integration.zip.config.xml; diff --git a/spring-integration-zip/src/main/java/org/springframework/integration/zip/package-info.java b/spring-integration-zip/src/main/java/org/springframework/integration/zip/package-info.java new file mode 100644 index 0000000000..96d4adb7be --- /dev/null +++ b/spring-integration-zip/src/main/java/org/springframework/integration/zip/package-info.java @@ -0,0 +1,4 @@ +/** + * Root package of the Zip Module. + */ +package org.springframework.integration.zip; diff --git a/spring-integration-zip/src/main/java/org/springframework/integration/zip/splitter/UnZipResultSplitter.java b/spring-integration-zip/src/main/java/org/springframework/integration/zip/splitter/UnZipResultSplitter.java new file mode 100644 index 0000000000..c909f67d80 --- /dev/null +++ b/spring-integration-zip/src/main/java/org/springframework/integration/zip/splitter/UnZipResultSplitter.java @@ -0,0 +1,66 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.zip.splitter; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.commons.io.FilenameUtils; + +import org.springframework.integration.file.FileHeaders; +import org.springframework.integration.splitter.AbstractMessageSplitter; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.integration.zip.ZipHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.util.Assert; + +/** + * + * @author Gunnar Hillert + * @author Andriy Kryvtsun + * @author Artem Bilan + * + * @since 6.1 + */ +public class UnZipResultSplitter extends AbstractMessageSplitter { + + @Override + @SuppressWarnings("unchecked") + protected Object splitMessage(Message message) { + Assert.state(message.getPayload() instanceof Map, + "The UnZipResultSplitter supports only Map payload"); + Map unzippedEntries = (Map) message.getPayload(); + MessageHeaders headers = message.getHeaders(); + + List> messageBuilders = new ArrayList<>(unzippedEntries.size()); + + for (Map.Entry entry : unzippedEntries.entrySet()) { + String path = FilenameUtils.getPath(entry.getKey()); + String filename = FilenameUtils.getName(entry.getKey()); + MessageBuilder messageBuilder = MessageBuilder.withPayload(entry.getValue()) + .setHeader(FileHeaders.FILENAME, filename) + .setHeader(ZipHeaders.ZIP_ENTRY_PATH, path) + .copyHeadersIfAbsent(headers); + messageBuilders.add(messageBuilder); + } + + return messageBuilders; + } + +} diff --git a/spring-integration-zip/src/main/java/org/springframework/integration/zip/splitter/package-info.java b/spring-integration-zip/src/main/java/org/springframework/integration/zip/splitter/package-info.java new file mode 100644 index 0000000000..a07528c86a --- /dev/null +++ b/spring-integration-zip/src/main/java/org/springframework/integration/zip/splitter/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes to support Splitter pattern for Zip. + */ +package org.springframework.integration.zip.splitter; diff --git a/spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/AbstractZipTransformer.java b/spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/AbstractZipTransformer.java new file mode 100644 index 0000000000..58639af1e9 --- /dev/null +++ b/spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/AbstractZipTransformer.java @@ -0,0 +1,117 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.zip.transformer; + +import java.io.File; +import java.nio.charset.Charset; + +import org.springframework.integration.file.DefaultFileNameGenerator; +import org.springframework.integration.file.FileNameGenerator; +import org.springframework.integration.transformer.AbstractTransformer; +import org.springframework.messaging.Message; +import org.springframework.util.Assert; + +/** + * Base class for transformers that provide Zip compression. + * + * @author Gunnar Hillert + * @author Artem Bilan + * + * @since 6.1 + */ +public abstract class AbstractZipTransformer extends AbstractTransformer { + + protected Charset charset = Charset.defaultCharset(); + + protected FileNameGenerator fileNameGenerator; + + protected ZipResultType zipResultType = ZipResultType.FILE; + + protected File workDirectory = + new File(System.getProperty("java.io.tmpdir") + File.separator + "ziptransformer"); + + protected boolean deleteFiles; + + /** + * If the payload is an instance of {@link File}, this property specifies + * whether to delete the {@link File} after transformation. + * Default is false. + * @param deleteFiles Defaults to false if not set + */ + public void setDeleteFiles(boolean deleteFiles) { + this.deleteFiles = deleteFiles; + } + + /** + * Set the work-directory. The work directory is used when the {@link ZipResultType} + * is set to {@link ZipResultType#FILE}. By default, this property is set to + * the System temporary directory containing a subdirectory "ziptransformer". + * @param workDirectory Must not be null and must not represent a file. + */ + public void setWorkDirectory(File workDirectory) { + Assert.notNull(workDirectory, "workDirectory must not be null."); + Assert.isTrue(workDirectory.isDirectory(), "The workDirectory specified must be a directory."); + this.workDirectory = workDirectory; + } + + /** + * Define the format of the data returned after transformation. Available + * options are: + *
    + *
  • File
  • + *
  • Byte Array
  • + *
+ * Defaults to {@link ZipResultType#FILE}. + * @param zipResultType Must not be null + */ + public void setZipResultType(ZipResultType zipResultType) { + Assert.notNull(zipResultType, "The zipResultType must not be empty."); + this.zipResultType = zipResultType; + } + + @Override + protected void onInit() { + super.onInit(); + + if (!this.workDirectory.exists()) { + logger.info(() -> "Creating work directory: " + this.workDirectory); + Assert.isTrue(this.workDirectory.mkdirs(), () -> "Can't create the 'workDirectory': " + this.workDirectory); + } + DefaultFileNameGenerator defaultFileNameGenerator = new DefaultFileNameGenerator(); + defaultFileNameGenerator.setBeanFactory(getBeanFactory()); + defaultFileNameGenerator.setConversionService(getConversionService()); + this.fileNameGenerator = defaultFileNameGenerator; + + } + + /** + * @param message the message and its payload must not be null. + */ + @Override + protected Object doTransform(Message message) { + return doZipTransform(message); + } + + /** + * Subclasses must implement this method to provide the Zip transformation + * logic. + * @param message The message will never be null. + * @return The result of the Zip transformation. + */ + protected abstract Object doZipTransform(Message message); + +} diff --git a/spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/SpringZipUtils.java b/spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/SpringZipUtils.java new file mode 100644 index 0000000000..071d1f3f89 --- /dev/null +++ b/spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/SpringZipUtils.java @@ -0,0 +1,137 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.zip.transformer; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Collection; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +import org.apache.commons.io.IOUtils; +import org.zeroturnaround.zip.ZipEntrySource; +import org.zeroturnaround.zip.ZipException; + +import org.springframework.core.log.LogAccessor; + +/** + * Once the Spring Integration Zip support matures, we need to contribute the + * methods in this utility class back to the ZT Zip project. + * + * @author Gunnar Hillert + * + * @since 6.1 + */ +public class SpringZipUtils { + + private static final LogAccessor logger = new LogAccessor(SpringZipUtils.class); + + public static byte[] pack(Collection entries, int compressionLevel) { + logger.debug(() -> "Creating byte array from: " + entries); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + pack(entries, outputStream, compressionLevel); + return outputStream.toByteArray(); + } + + public static void pack(Collection entries, File zip, int compressionLevel) { + logger.debug(() -> "Creating '" + zip + "' from " + entries + "."); + + FileOutputStream outputStream; + try { + outputStream = new FileOutputStream(zip); + } + catch (FileNotFoundException e) { + throw new IllegalStateException(String.format("File '%s' not found.", zip.getAbsolutePath()), e); + } + pack(entries, outputStream, compressionLevel); + + } + + private static void pack(Collection entries, OutputStream outputStream, int compressionLevel) { + + ZipOutputStream out = null; + final BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream); + + try { + out = new ZipOutputStream(bufferedOutputStream); + out.setLevel(compressionLevel); + for (ZipEntrySource entry : entries) { + addEntry(entry, out); + } + } + catch (IOException e) { + throw rethrow(e); + } + finally { + IOUtils.closeQuietly(out); + } + + } + + private static void addEntry(ZipEntrySource entry, ZipOutputStream out) throws IOException { + out.putNextEntry(entry.getEntry()); + InputStream in = entry.getInputStream(); + if (in != null) { + try { + IOUtils.copy(in, out); + } + finally { + IOUtils.closeQuietly(in); + } + } + out.closeEntry(); + } + + private static ZipException rethrow(IOException e) { + throw new ZipException(e); + } + + public static void copy(InputStream in, File file) throws IOException { + OutputStream out = new BufferedOutputStream(new FileOutputStream(file)); + try { + IOUtils.copy(in, out); + } + finally { + IOUtils.closeQuietly(out); + } + } + + public byte[] copy(InputStream in) throws IOException { + return IOUtils.toByteArray(in); + } + + static boolean isValid(final File file) { + ZipFile zipfile = null; + try { + zipfile = new ZipFile(file); + return true; + } + catch (IOException e) { + return false; + } + finally { + IOUtils.closeQuietly(zipfile); + } + } + +} diff --git a/spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/UnZipTransformer.java b/spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/UnZipTransformer.java new file mode 100644 index 0000000000..95ea07bbac --- /dev/null +++ b/spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/UnZipTransformer.java @@ -0,0 +1,205 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.zip.transformer; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.zip.ZipEntry; + +import org.apache.commons.io.IOUtils; +import org.zeroturnaround.zip.ZipEntryCallback; +import org.zeroturnaround.zip.ZipException; +import org.zeroturnaround.zip.ZipUtil; + +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHandlingException; +import org.springframework.messaging.MessagingException; + +/** + * Transformer implementation that applies an UnZip transformation to the message + * payload. + * + * @author Gunnar Hillert + * @author Artem Bilan + * @author Ingo Dueppe + * + * @since 6.1 + */ +public class UnZipTransformer extends AbstractZipTransformer { + + private volatile boolean expectSingleResult = false; + + /** + * + * This parameter indicates that only one result object shall be returned as + * a result from the executed Unzip operation. If set to true and + * more than 1 element is returned, then that + * 1 element is extracted and returned as payload. + * If the result map contains more than 1 element and + * {@link #expectSingleResult} is true, then a + * {@link MessagingException} is thrown. + * If set to false, the complete result list is returned as the + * payload. This is the {@code default}. + * @param expectSingleResult If not set explicitly, will default to false + */ + public void setExpectSingleResult(boolean expectSingleResult) { + this.expectSingleResult = expectSingleResult; + } + + @Override + protected Object doZipTransform(final Message message) { + try { + Object payload = message.getPayload(); + Object unzippedData; + + InputStream inputStream = null; + + try { + if (payload instanceof final File filePayload) { + if (filePayload.isDirectory()) { + throw new UnsupportedOperationException("Cannot unzip a directory: " + + filePayload.getAbsolutePath()); + } + + if (!SpringZipUtils.isValid(filePayload)) { + throw new IllegalStateException("Not a zip file: " + filePayload.getAbsolutePath()); + } + + inputStream = new FileInputStream(filePayload); + } + else if (payload instanceof InputStream) { + inputStream = (InputStream) payload; + } + else if (payload instanceof byte[]) { + inputStream = new ByteArrayInputStream((byte[]) payload); + } + else { + throw new IllegalArgumentException("Unsupported payload type '" + payload.getClass().getSimpleName() + + "'. The only supported payload types are java.io.File, byte[] and java.io.InputStream"); + } + + final SortedMap uncompressedData = new TreeMap<>(); + + ZipUtil.iterate(inputStream, new ZipEntryCallback() { + + @Override + public void process(InputStream zipEntryInputStream, ZipEntry zipEntry) throws IOException { + + final String zipEntryName = zipEntry.getName(); + final long zipEntryTime = zipEntry.getTime(); + final long zipEntryCompressedSize = zipEntry.getCompressedSize(); + final String type = zipEntry.isDirectory() ? "directory" : "file"; + + logger.info(() -> String.format("Unpacking Zip Entry - Name: '%s',Time: '%s', " + + "Compressed Size: '%s', Type: '%s'", + zipEntryName, zipEntryTime, zipEntryCompressedSize, type)); + + if (ZipResultType.FILE.equals(zipResultType)) { + final File destinationFile = checkPath(message, zipEntryName); + + if (zipEntry.isDirectory()) { + destinationFile.mkdirs(); //NOSONAR false positive + } + else { + mkDirOfAncestorDirectories(destinationFile); + SpringZipUtils.copy(zipEntryInputStream, destinationFile); + uncompressedData.put(zipEntryName, destinationFile); + } + } + else if (ZipResultType.BYTE_ARRAY.equals(zipResultType)) { + if (!zipEntry.isDirectory()) { + checkPath(message, zipEntryName); + byte[] data = IOUtils.toByteArray(zipEntryInputStream); + uncompressedData.put(zipEntryName, data); + } + } + else { + throw new IllegalStateException("Unsupported zipResultType: " + zipResultType); + } + } + + public File checkPath(final Message message, final String zipEntryName) throws IOException { + final File tempDir = new File(workDirectory, message.getHeaders().getId().toString()); + tempDir.mkdirs(); //NOSONAR false positive + final File destinationFile = new File(tempDir, zipEntryName); + + /* If we see the relative traversal string of ".." we need to make sure + * that the outputdir + name doesn't leave the outputdir. + */ + if (!destinationFile.getCanonicalPath() + .startsWith(tempDir.getCanonicalPath() + File.separator)) { + + throw new ZipException("The file " + zipEntryName + + " is trying to leave the target output directory of " + workDirectory); + } + return destinationFile; + } + }); + + if (uncompressedData.isEmpty()) { + logger.warn(() -> "No data unzipped from payload with message Id " + message.getHeaders().getId()); + unzippedData = null; + } + else { + + if (this.expectSingleResult) { + if (uncompressedData.size() == 1) { + unzippedData = uncompressedData.values().iterator().next(); + } + else { + throw new MessagingException(message, + String.format("The UnZip operation extracted %s " + + "result objects but expectSingleResult was 'true'.", uncompressedData + .size())); + } + } + else { + unzippedData = uncompressedData; + } + + } + + IOUtils.closeQuietly(inputStream); + if (payload instanceof final File filePayload && this.deleteFiles) { + if (!filePayload.delete() && logger.isWarnEnabled()) { + logger.warn(() -> "failed to delete File '" + filePayload + "'"); + } + } + } + finally { + IOUtils.closeQuietly(inputStream); + } + return unzippedData; + } + catch (Exception e) { + throw new MessageHandlingException(message, "Failed to apply Zip transformation.", e); + } + } + + private static void mkDirOfAncestorDirectories(File destinationFile) { + File parentDirectory = destinationFile.getParentFile(); + if (parentDirectory != null) { + parentDirectory.mkdirs(); + } + } + +} diff --git a/spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/ZipResultType.java b/spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/ZipResultType.java new file mode 100644 index 0000000000..6b09e01b73 --- /dev/null +++ b/spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/ZipResultType.java @@ -0,0 +1,28 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.zip.transformer; + +/** + * @author Gunnar Hillert + * + * @since 6.1 + */ +public enum ZipResultType { + + FILE, BYTE_ARRAY + +} diff --git a/spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/ZipTransformer.java b/spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/ZipTransformer.java new file mode 100644 index 0000000000..dc789d47c7 --- /dev/null +++ b/spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/ZipTransformer.java @@ -0,0 +1,223 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.zip.transformer; + +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.zip.Deflater; + +import org.apache.commons.io.FilenameUtils; +import org.zeroturnaround.zip.ByteSource; +import org.zeroturnaround.zip.FileSource; +import org.zeroturnaround.zip.ZipEntrySource; + +import org.springframework.integration.file.FileHeaders; +import org.springframework.integration.transformer.Transformer; +import org.springframework.integration.zip.ZipHeaders; +import org.springframework.messaging.Message; +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StringUtils; + +/** + * {@link Transformer} implementation that applies a Zip transformation to the + * message payload. Keep in mind that Zip entry timestamps are recorded only to + * two 2 second precision: + *

+ * See also: + *

+ * If you want to generate Zip files larger than {@code 4GB}, you must use Java 7: + *

+ * See also: + * + * @author Gunnar Hillert + * @author Artem Bilan + * + * @since 6.1 + */ +public class ZipTransformer extends AbstractZipTransformer { + + private static final String ZIP_EXTENSION = ".zip"; + + private volatile int compressionLevel = Deflater.DEFAULT_COMPRESSION; + + private volatile boolean useFileAttributes = true; + + /** + * Set the compression level. Default is {@link Deflater#DEFAULT_COMPRESSION}. + * @param compressionLevel Must be an integer value from 0-9. + */ + public void setCompressionLevel(int compressionLevel) { + Assert.isTrue(compressionLevel >= 0 && compressionLevel <= 9, "Acceptable levels are 0-9"); + this.compressionLevel = compressionLevel; + } + + /** + * Specify whether the name of the file shall be used for the zip entry. + * @param useFileAttributes Defaults to true if not set explicitly + */ + public void setUseFileAttributes(boolean useFileAttributes) { + this.useFileAttributes = useFileAttributes; + } + + /** + * The payload may encompass the following types: + *

+ * When providing an {@link Iterable}, nested Iterables are not supported. However, + * payloads can be any of the other supported types. + */ + @Override + protected Object doZipTransform(Message message) { + final Object payload = message.getPayload(); + final Object zippedData; + final String baseFileName = this.fileNameGenerator.generateFileName(message); + + final String zipEntryName; + final String zipFileName; + + if (message.getHeaders().containsKey(ZipHeaders.ZIP_ENTRY_FILE_NAME)) { + zipEntryName = (String) message.getHeaders().get(ZipHeaders.ZIP_ENTRY_FILE_NAME); + } + else { + zipEntryName = baseFileName; + } + + if (message.getHeaders().containsKey(FileHeaders.FILENAME)) { + zipFileName = (String) message.getHeaders().get(FileHeaders.FILENAME); + } + else { + zipFileName = baseFileName + ZIP_EXTENSION; + } + + final Date lastModifiedDate; + + if (message.getHeaders().containsKey(ZipHeaders.ZIP_ENTRY_LAST_MODIFIED_DATE)) { + lastModifiedDate = (Date) message.getHeaders().get(ZipHeaders.ZIP_ENTRY_LAST_MODIFIED_DATE); + } + else { + lastModifiedDate = new Date(); + } + + java.util.List entries = new ArrayList<>(); + + if (payload instanceof Iterable) { + int counter = 1; + + String baseName = FilenameUtils.getBaseName(zipEntryName); + String fileExtension = FilenameUtils.getExtension(zipEntryName); + + if (StringUtils.hasText(fileExtension)) { + fileExtension = FilenameUtils.EXTENSION_SEPARATOR_STR + fileExtension; + } + + for (Object item : (Iterable) payload) { + + final ZipEntrySource zipEntrySource = createZipEntrySource(item, lastModifiedDate, baseName + "_" + + counter + fileExtension, this.useFileAttributes); + logger.debug(() -> "ZipEntrySource path: '" + zipEntrySource.getPath() + "'"); + entries.add(zipEntrySource); + counter++; + } + } + else { + final ZipEntrySource zipEntrySource = + createZipEntrySource(payload, lastModifiedDate, zipEntryName, this.useFileAttributes); + entries.add(zipEntrySource); + } + + final byte[] zippedBytes = SpringZipUtils.pack(entries, this.compressionLevel); + + if (ZipResultType.FILE.equals(this.zipResultType)) { + final File zippedFile = new File(this.workDirectory, zipFileName); + try { + FileCopyUtils.copy(zippedBytes, zippedFile); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + zippedData = zippedFile; + } + else if (ZipResultType.BYTE_ARRAY.equals(this.zipResultType)) { + zippedData = zippedBytes; + } + else { + throw new IllegalStateException("Unsupported zipResultType " + this.zipResultType); + } + + if (this.deleteFiles) { + if (payload instanceof Iterable) { + for (Object item : (Iterable) payload) { + deleteFile(item); + } + } + else { + deleteFile(payload); + } + } + return getMessageBuilderFactory() + .withPayload(zippedData) + .copyHeaders(message.getHeaders()) + .setHeader(FileHeaders.FILENAME, zipFileName) + .build(); + } + + private void deleteFile(Object fileToDelete) { + if (fileToDelete instanceof File && !((File) fileToDelete).delete()) { + logger.warn(() -> "Failed to delete File '" + fileToDelete + "'"); + } + } + + private ZipEntrySource createZipEntrySource(Object item, + Date lastModifiedDate, String zipEntryName, boolean useFileAttributes) { + + if (item instanceof final File filePayload) { + String fileName = useFileAttributes ? filePayload.getName() : zipEntryName; + + if (((File) item).isDirectory()) { + throw new UnsupportedOperationException("Zipping of directories is not supported."); + } + + return new FileSource(fileName, filePayload); + + } + else if (item instanceof byte[] || item instanceof String) { + byte[] bytesToCompress; + + if (item instanceof String) { + bytesToCompress = ((String) item).getBytes(this.charset); + } + else { + bytesToCompress = (byte[]) item; + } + + return new ByteSource(zipEntryName, bytesToCompress, lastModifiedDate.getTime()); + } + else { + throw new IllegalArgumentException("Unsupported payload type. The only supported payloads are " + + "java.io.File, java.lang.String, and byte[]"); + } + } + +} diff --git a/spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/package-info.java b/spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/package-info.java new file mode 100644 index 0000000000..0ab158fa9a --- /dev/null +++ b/spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes to support Transformer pattern for Zip. + */ +package org.springframework.integration.zip.transformer; diff --git a/spring-integration-zip/src/main/resources/META-INF/spring.handlers b/spring-integration-zip/src/main/resources/META-INF/spring.handlers new file mode 100644 index 0000000000..fb31de6ae2 --- /dev/null +++ b/spring-integration-zip/src/main/resources/META-INF/spring.handlers @@ -0,0 +1 @@ +http\://www.springframework.org/schema/integration/zip=org.springframework.integration.zip.config.xml.ZipNamespaceHandler diff --git a/spring-integration-zip/src/main/resources/META-INF/spring.schemas b/spring-integration-zip/src/main/resources/META-INF/spring.schemas new file mode 100644 index 0000000000..72c60f875e --- /dev/null +++ b/spring-integration-zip/src/main/resources/META-INF/spring.schemas @@ -0,0 +1,4 @@ +http\://www.springframework.org/schema/integration/zip/spring-integration-zip-1.0.xsd=org/springframework/integration/zip/config/spring-integration-zip.xsd +http\://www.springframework.org/schema/integration/zip/spring-integration-zip.xsd=org/springframework/integration/zip/config/spring-integration-zip.xsd +https\://www.springframework.org/schema/integration/zip/spring-integration-zip-1.0.xsd=org/springframework/integration/zip/config/spring-integration-zip.xsd +https\://www.springframework.org/schema/integration/zip/spring-integration-zip.xsd=org/springframework/integration/zip/config/spring-integration-zip.xsd diff --git a/spring-integration-zip/src/main/resources/META-INF/spring.tooling b/spring-integration-zip/src/main/resources/META-INF/spring.tooling new file mode 100644 index 0000000000..990490a758 --- /dev/null +++ b/spring-integration-zip/src/main/resources/META-INF/spring.tooling @@ -0,0 +1,4 @@ +# Tooling related information for the integration Zip namespace +http\://www.springframework.org/schema/integration/zip@name=integration Zip Namespace +http\://www.springframework.org/schema/integration/zip@prefix=int-zip +http\://www.springframework.org/schema/integration/zip@icon=org/springframework/integration/zip/config/spring-integration-zip.gif diff --git a/spring-integration-zip/src/main/resources/org/springframework/integration/zip/config/spring-integration-zip.gif b/spring-integration-zip/src/main/resources/org/springframework/integration/zip/config/spring-integration-zip.gif new file mode 100644 index 0000000000..210e0764fa Binary files /dev/null and b/spring-integration-zip/src/main/resources/org/springframework/integration/zip/config/spring-integration-zip.gif differ diff --git a/spring-integration-zip/src/main/resources/org/springframework/integration/zip/config/spring-integration-zip.xsd b/spring-integration-zip/src/main/resources/org/springframework/integration/zip/config/spring-integration-zip.xsd new file mode 100644 index 0000000000..d9d397cf91 --- /dev/null +++ b/spring-integration-zip/src/main/resources/org/springframework/integration/zip/config/spring-integration-zip.xsd @@ -0,0 +1,126 @@ + + + + + + + + + + Defines the configuration elements for the Spring Integration + Zip Adapter. + + + + + + + Creates a Transformer that compresses message + payloads using Zip compressions. The following payload types are + supported: + + - java.io.File + - byte[] + - String + + + + + + + + + + Sets the compression level. Default is + java.util.zip.Deflater.DEFAULT_COMPRESSION + + + + + + + + + + + + Creates a Transformer that decompresses message + payloads using Zip compressions. The following payload types are + supported: + + - java.io.File + - byte[] + + + + + + + + + + + + + + + + + + + + Identifies the underlying Spring bean definition (EventDrivenConsumer) + + + + + + + + If the payload is an instance of {@link File}, this + attribute specifies whether to delete the {@link File} after + transformation. The default is 'false'. + + + + + + + If set to 'true', this property indicates + that only one result object shall be returned as a result + from the executed Unzip operation. Defaults to 'false'. + + + + + + + Defines the format of the data returned after + transformation. Available options are: + - File + - Byte Array + + Depending on the used input format, not all + options may be applicable. + + + + + + + + + + + + + + + + diff --git a/spring-integration-zip/src/test/java/org/springframework/integration/zip/UnZip2FileTests-context.xml b/spring-integration-zip/src/test/java/org/springframework/integration/zip/UnZip2FileTests-context.xml new file mode 100644 index 0000000000..13b2cacd35 --- /dev/null +++ b/spring-integration-zip/src/test/java/org/springframework/integration/zip/UnZip2FileTests-context.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-zip/src/test/java/org/springframework/integration/zip/UnZip2FileTests.java b/spring-integration-zip/src/test/java/org/springframework/integration/zip/UnZip2FileTests.java new file mode 100644 index 0000000000..9074dd9d4e --- /dev/null +++ b/spring-integration-zip/src/test/java/org/springframework/integration/zip/UnZip2FileTests.java @@ -0,0 +1,160 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.zip; + +import java.io.File; +import java.io.InputStream; + +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.zeroturnaround.zip.ZipException; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.core.io.Resource; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.integration.transformer.MessageTransformationException; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * + * @author Gunnar Hillert + * @author Artem Bilan + * + * @since 6.1 + */ +@SpringJUnitConfig +@DirtiesContext +public class UnZip2FileTests { + + @Autowired + private ApplicationContext context; + + @Autowired + private MessageChannel input; + + @TempDir + public static File workDir; + + @BeforeAll + public static void setup() { + System.setProperty("workDir", workDir.getAbsolutePath()); + } + + @BeforeEach + public void cleanUp() { + cleanupDirectory(workDir); + } + + @Test + public void unZipWithOneEntry() throws Exception { + final Resource resource = this.context.getResource("classpath:testzipdata/single.zip"); + final InputStream is = resource.getInputStream(); + + byte[] zipdata = IOUtils.toByteArray(is); + is.close(); + + final Message message = MessageBuilder.withPayload(zipdata).build(); + + input.send(message); + + assertThat(workDir.list()).hasSize(1); + + File fileInWorkDir = workDir.listFiles()[0]; + + assertThat(fileInWorkDir).isFile(); + assertThat(fileInWorkDir).hasName("single.txt"); + } + + @Test + public void unZipWithMultipleEntries() throws Exception { + final Resource resource = this.context.getResource("classpath:testzipdata/countries.zip"); + final InputStream is = resource.getInputStream(); + + byte[] zipdata = IOUtils.toByteArray(is); + is.close(); + + final Message message = MessageBuilder.withPayload(zipdata).build(); + + input.send(message); + + assertThat(workDir.list()).hasSize(4); + + File[] files = workDir.listFiles(); + + boolean continents = false; + boolean de = false; + boolean fr = false; + boolean pl = false; + + for (File file : files) { + if (file.getName().equals("continents")) { + continents = true; + assertThat(file).isDirectory(); + assertThat(file.list()).hasSize(2); + } + if (file.getName().equals("de.txt")) { + de = true; + assertThat(file).isFile(); + } + if (file.getName().equals("fr.txt")) { + fr = true; + assertThat(file).isFile(); + } + if (file.getName().equals("pl.txt")) { + pl = true; + assertThat(file).isFile(); + } + } + + assertThat(continents).isTrue(); + assertThat(de).isTrue(); + assertThat(fr).isTrue(); + assertThat(pl).isTrue(); + } + + @Test + public void unZipTraversal() throws Exception { + final Resource resource = this.context.getResource("classpath:testzipdata/zip-malicious-traversal.zip"); + final InputStream is = resource.getInputStream(); + byte[] zipdata = IOUtils.toByteArray(is); + final Message message = MessageBuilder.withPayload(zipdata).build(); + assertThatExceptionOfType(MessageTransformationException.class) + .isThrownBy(() -> input.send(message)) + .withRootCauseInstanceOf(ZipException.class) + .withStackTraceContaining("is trying to leave the target output directory"); + } + + private static void cleanupDirectory(File dir) { + for (File file: dir.listFiles()) { + if (file.isDirectory()) { + cleanupDirectory(file); + } + file.delete(); + } + } + +} diff --git a/spring-integration-zip/src/test/java/org/springframework/integration/zip/Zip2FileTests-context.xml b/spring-integration-zip/src/test/java/org/springframework/integration/zip/Zip2FileTests-context.xml new file mode 100644 index 0000000000..39fe5e3c79 --- /dev/null +++ b/spring-integration-zip/src/test/java/org/springframework/integration/zip/Zip2FileTests-context.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + diff --git a/spring-integration-zip/src/test/java/org/springframework/integration/zip/Zip2FileTests.java b/spring-integration-zip/src/test/java/org/springframework/integration/zip/Zip2FileTests.java new file mode 100644 index 0000000000..251f08c8f3 --- /dev/null +++ b/spring-integration-zip/src/test/java/org/springframework/integration/zip/Zip2FileTests.java @@ -0,0 +1,158 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.zip; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.integration.file.FileHeaders; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * + * @author Gunnar Hillert + * @author Artem Bilan + * + * @since 6.1 + */ +@SpringJUnitConfig +@DirtiesContext +public class Zip2FileTests { + + @Autowired + private MessageChannel input; + + @TempDir + public static File workDir; + + @BeforeAll + public static void setup() { + System.setProperty("workDir", workDir.getAbsolutePath()); + } + + @BeforeEach + public void cleanUp() { + for (File file : workDir.listFiles()) { + file.delete(); + } + } + + @Test + public void zipStringWithDefaultFileName() { + final Message message = MessageBuilder.withPayload("Zip me up.").build(); + input.send(message); + assertThat(workDir.list()).hasSize(1); + + File fileInWorkDir = workDir.listFiles()[0]; + + assertThat(fileInWorkDir.isFile()).isTrue(); + assertThat(fileInWorkDir.getName()).contains(message.getHeaders().getId().toString()); + assertThat(fileInWorkDir.getName()).endsWith(".zip"); + } + + @Test + public void zipStringWithExplicitFileName() { + input.send(MessageBuilder.withPayload("Zip me up.") + .setHeader(FileHeaders.FILENAME, "zipString.zip") + .build()); + + assertThat(workDir.list()).hasSize(1); + assertThat(workDir.list()[0]).isEqualTo("zipString.zip"); + } + + @Test + public void zipBytesWithExplicitFileName() { + input.send(MessageBuilder.withPayload("Zip me up.".getBytes()) + .setHeader(FileHeaders.FILENAME, "zipString.zip") + .build()); + + assertThat(workDir.list()).hasSize(1); + assertThat(workDir.list()[0]).isEqualTo("zipString.zip"); + } + + @Test + public void zipFile() throws IOException { + File fileToCompress = File.createTempFile("test1", "tmp"); + FileUtils.writeStringToFile(fileToCompress, "hello world", Charset.defaultCharset()); + + input.send(MessageBuilder.withPayload(fileToCompress).build()); + + assertThat(workDir.list()).hasSize(1); + assertThat(workDir.list()[0]).isEqualTo(fileToCompress.getName() + ".zip"); + } + + @Test + public void zipIterableWithMultipleStrings() { + String stringToCompress1 = "String1"; + String stringToCompress2 = "String2"; + String stringToCompress3 = "String3"; + String stringToCompress4 = "String4"; + + List stringsToCompress = new ArrayList<>(4); + + stringsToCompress.add(stringToCompress1); + stringsToCompress.add(stringToCompress2); + stringsToCompress.add(stringToCompress3); + stringsToCompress.add(stringToCompress4); + + input.send(MessageBuilder.withPayload(stringsToCompress) + .setHeader(FileHeaders.FILENAME, "zipWith4Strings.zip") + .build()); + + assertThat(workDir.list()).hasSize(1); + assertThat(workDir.list()[0]).isEqualTo("zipWith4Strings.zip"); + } + + @Test + public void zipIterableWithDifferentTypes() throws IOException { + + String stringToCompress = "String1"; + byte[] bytesToCompress = "String2".getBytes(); + File fileToCompress = File.createTempFile("test2", "tmp"); + FileUtils.writeStringToFile(fileToCompress, "hello world", Charset.defaultCharset()); + + final List objectsToCompress = new ArrayList<>(3); + + objectsToCompress.add(stringToCompress); + objectsToCompress.add(bytesToCompress); + objectsToCompress.add(fileToCompress); + + input.send(MessageBuilder.withPayload(objectsToCompress) + .setHeader(FileHeaders.FILENAME, "objects-to-compress.zip") + .build()); + + assertThat(workDir.list()).hasSize(1); + assertThat(workDir.list()[0]).isEqualTo("objects-to-compress.zip"); + } + +} diff --git a/spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/UnZipTransformerParserTests-context.xml b/spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/UnZipTransformerParserTests-context.xml new file mode 100644 index 0000000000..484ebc5c26 --- /dev/null +++ b/spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/UnZipTransformerParserTests-context.xml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/UnZipTransformerParserTests.java b/spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/UnZipTransformerParserTests.java new file mode 100644 index 0000000000..2a0dc0e331 --- /dev/null +++ b/spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/UnZipTransformerParserTests.java @@ -0,0 +1,129 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.zip.config.xml; + +import java.io.File; +import java.nio.charset.Charset; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.integration.channel.AbstractMessageChannel; +import org.springframework.integration.endpoint.EventDrivenConsumer; +import org.springframework.integration.file.DefaultFileNameGenerator; +import org.springframework.integration.file.FileNameGenerator; +import org.springframework.integration.test.util.TestUtils; +import org.springframework.integration.transformer.MessageTransformingHandler; +import org.springframework.integration.zip.transformer.UnZipTransformer; +import org.springframework.integration.zip.transformer.ZipResultType; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * + * @author Gunnar Hillert + * @author Artem Bilan + * + * @since 6.1 + */ +@SpringJUnitConfig +@DirtiesContext +public class UnZipTransformerParserTests { + + @Autowired + private ConfigurableApplicationContext context; + + @Test + public void testUnZipTransformerParserWithDefaults() { + EventDrivenConsumer consumer = this.context.getBean("unzipTransformerWithDefaults", EventDrivenConsumer.class); + + final AbstractMessageChannel inputChannel = TestUtils.getPropertyValue(consumer, "inputChannel", AbstractMessageChannel.class); + assertThat(inputChannel.getComponentName()).isEqualTo("input"); + + final MessageTransformingHandler handler = TestUtils.getPropertyValue(consumer, "handler", MessageTransformingHandler.class); + + assertThat(TestUtils.getPropertyValue(handler, "outputChannelName")).isEqualTo("output"); + + final UnZipTransformer unZipTransformer = TestUtils.getPropertyValue(handler, "transformer", UnZipTransformer.class); + + final Charset charset = TestUtils.getPropertyValue(unZipTransformer, "charset", Charset.class); + final FileNameGenerator fileNameGenerator = TestUtils.getPropertyValue(unZipTransformer, "fileNameGenerator", FileNameGenerator.class); + final ZipResultType zipResultType = TestUtils.getPropertyValue(unZipTransformer, "zipResultType", ZipResultType.class); + final File workDirectory = TestUtils.getPropertyValue(unZipTransformer, "workDirectory", File.class); + final Boolean deleteFiles = TestUtils.getPropertyValue(unZipTransformer, "deleteFiles", Boolean.class); + final Boolean expectSingleResult = TestUtils.getPropertyValue(unZipTransformer, "expectSingleResult", Boolean.class); + + assertThat(charset).isNotNull(); + assertThat(fileNameGenerator).isNotNull(); + assertThat(zipResultType).isNotNull(); + assertThat(workDirectory).isNotNull(); + assertThat(deleteFiles).isNotNull(); + assertThat(expectSingleResult).isNotNull(); + + assertThat(charset).isEqualTo(Charset.defaultCharset()); + assertThat(fileNameGenerator).isInstanceOf(DefaultFileNameGenerator.class); + assertThat(zipResultType).isEqualTo(ZipResultType.FILE); + assertThat(workDirectory) + .isEqualTo(new File(System.getProperty("java.io.tmpdir") + File.separator + "ziptransformer")); + assertThat(workDirectory.exists()).isTrue(); + assertThat(workDirectory.isDirectory()).isTrue(); + assertThat(deleteFiles).isFalse(); + assertThat(expectSingleResult).isFalse(); + } + + @Test + public void testUnZipTransformerParserWithExplicitSettings() { + EventDrivenConsumer consumer = this.context.getBean("unzipTransformer", EventDrivenConsumer.class); + + final AbstractMessageChannel inputChannel = TestUtils.getPropertyValue(consumer, "inputChannel", AbstractMessageChannel.class); + assertThat(inputChannel.getComponentName()).isEqualTo("input"); + + final MessageTransformingHandler handler = TestUtils.getPropertyValue(consumer, "handler", MessageTransformingHandler.class); + + assertThat(TestUtils.getPropertyValue(handler, "outputChannelName")).isEqualTo("output"); + + final UnZipTransformer unZipTransformer = TestUtils.getPropertyValue(handler, "transformer", UnZipTransformer.class); + + final Charset charset = TestUtils.getPropertyValue(unZipTransformer, "charset", Charset.class); + final FileNameGenerator fileNameGenerator = TestUtils.getPropertyValue(unZipTransformer, "fileNameGenerator", FileNameGenerator.class); + final ZipResultType zipResultType = TestUtils.getPropertyValue(unZipTransformer, "zipResultType", ZipResultType.class); + final File workDirectory = TestUtils.getPropertyValue(unZipTransformer, "workDirectory", File.class); + final Boolean deleteFiles = TestUtils.getPropertyValue(unZipTransformer, "deleteFiles", Boolean.class); + final Boolean expectSingleResult = TestUtils.getPropertyValue(unZipTransformer, "expectSingleResult", Boolean.class); + + assertThat(charset).isNotNull(); + assertThat(fileNameGenerator).isNotNull(); + assertThat(zipResultType).isNotNull(); + assertThat(workDirectory).isNotNull(); + assertThat(deleteFiles).isNotNull(); + assertThat(expectSingleResult).isNotNull(); + + assertThat(charset).isEqualTo(Charset.defaultCharset()); + assertThat(fileNameGenerator).isInstanceOf(DefaultFileNameGenerator.class); + assertThat(zipResultType).isEqualTo(ZipResultType.FILE); + assertThat(workDirectory) + .isEqualTo(new File(System.getProperty("java.io.tmpdir") + File.separator + "ziptransformer")); + assertThat(workDirectory.exists()).isTrue(); + assertThat(workDirectory.isDirectory()).isTrue(); + assertThat(deleteFiles).isTrue(); + assertThat(expectSingleResult).isTrue(); + } + +} diff --git a/spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/ZipTransformerParserTests-context.xml b/spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/ZipTransformerParserTests-context.xml new file mode 100644 index 0000000000..07b477e3e6 --- /dev/null +++ b/spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/ZipTransformerParserTests-context.xml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/ZipTransformerParserTests.java b/spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/ZipTransformerParserTests.java new file mode 100644 index 0000000000..399cf10ce2 --- /dev/null +++ b/spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/ZipTransformerParserTests.java @@ -0,0 +1,143 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.zip.config.xml; + +import java.io.File; +import java.nio.charset.Charset; +import java.util.zip.Deflater; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.integration.channel.AbstractMessageChannel; +import org.springframework.integration.endpoint.EventDrivenConsumer; +import org.springframework.integration.file.DefaultFileNameGenerator; +import org.springframework.integration.file.FileNameGenerator; +import org.springframework.integration.test.util.TestUtils; +import org.springframework.integration.transformer.MessageTransformingHandler; +import org.springframework.integration.zip.transformer.ZipResultType; +import org.springframework.integration.zip.transformer.ZipTransformer; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Gunnar Hillert + * @author Artem Bilan + * + * @since 6.1 + */ +@SpringJUnitConfig +@DirtiesContext +public class ZipTransformerParserTests { + + @Autowired + private ConfigurableApplicationContext context; + + @Test + public void testZipTransformerParserWithDefaults() { + EventDrivenConsumer consumer = this.context.getBean("zipTransformerWithDefaults", EventDrivenConsumer.class); + + final AbstractMessageChannel inputChannel = TestUtils.getPropertyValue(consumer, "inputChannel", AbstractMessageChannel.class); + assertThat(inputChannel.getComponentName()).isEqualTo("input"); + + final MessageTransformingHandler handler = TestUtils.getPropertyValue(consumer, "handler", MessageTransformingHandler.class); + + assertThat(TestUtils.getPropertyValue(handler, "outputChannelName")).isEqualTo("output"); + + final ZipTransformer zipTransformer = TestUtils.getPropertyValue(handler, "transformer", ZipTransformer.class); + + final Charset charset = TestUtils.getPropertyValue(zipTransformer, "charset", Charset.class); + final FileNameGenerator fileNameGenerator = TestUtils.getPropertyValue(zipTransformer, "fileNameGenerator", FileNameGenerator.class); + final ZipResultType zipResultType = TestUtils.getPropertyValue(zipTransformer, "zipResultType", ZipResultType.class); + final File workDirectory = TestUtils.getPropertyValue(zipTransformer, "workDirectory", File.class); + final Integer compressionLevel = TestUtils.getPropertyValue(zipTransformer, "compressionLevel", Integer.class); + final Boolean deleteFiles = TestUtils.getPropertyValue(zipTransformer, "deleteFiles", Boolean.class); + + assertThat(charset).isNotNull(); + assertThat(fileNameGenerator).isNotNull(); + assertThat(zipResultType).isNotNull(); + assertThat(workDirectory).isNotNull(); + assertThat(deleteFiles).isNotNull(); + assertThat(compressionLevel).isNotNull(); + + assertThat(charset).isEqualTo(Charset.defaultCharset()); + assertThat(fileNameGenerator).isInstanceOf(DefaultFileNameGenerator.class); + assertThat(zipResultType).isEqualTo(ZipResultType.FILE); + assertThat(workDirectory) + .isEqualTo(new File(System.getProperty("java.io.tmpdir") + File.separator + "ziptransformer")); + assertThat(workDirectory.exists()).isTrue(); + assertThat(workDirectory.isDirectory()).isTrue(); + assertThat(deleteFiles).isFalse(); + assertThat(compressionLevel).isEqualTo(Deflater.DEFAULT_COMPRESSION); + } + + @Test + public void testZipTransformerParserWithExplicitSettings() { + EventDrivenConsumer consumer = this.context.getBean("zipTransformer", EventDrivenConsumer.class); + + final AbstractMessageChannel inputChannel = TestUtils.getPropertyValue(consumer, "inputChannel", AbstractMessageChannel.class); + assertThat(inputChannel.getComponentName()).isEqualTo("input"); + + final MessageTransformingHandler handler = TestUtils.getPropertyValue(consumer, "handler", MessageTransformingHandler.class); + + assertThat(TestUtils.getPropertyValue(handler, "outputChannelName")).isEqualTo("output"); + + final ZipTransformer zipTransformer = TestUtils.getPropertyValue(handler, "transformer", ZipTransformer.class); + + final Charset charset = TestUtils.getPropertyValue(zipTransformer, "charset", Charset.class); + final FileNameGenerator fileNameGenerator = TestUtils.getPropertyValue(zipTransformer, "fileNameGenerator", FileNameGenerator.class); + final ZipResultType zipResultType = TestUtils.getPropertyValue(zipTransformer, "zipResultType", ZipResultType.class); + final File workDirectory = TestUtils.getPropertyValue(zipTransformer, "workDirectory", File.class); + final Integer compressionLevel = TestUtils.getPropertyValue(zipTransformer, "compressionLevel", Integer.class); + final Boolean deleteFiles = TestUtils.getPropertyValue(zipTransformer, "deleteFiles", Boolean.class); + + assertThat(charset).isNotNull(); + assertThat(fileNameGenerator).isNotNull(); + assertThat(zipResultType).isNotNull(); + assertThat(workDirectory).isNotNull(); + assertThat(deleteFiles).isNotNull(); + assertThat(compressionLevel).isNotNull(); + + assertThat(charset).isEqualTo(Charset.defaultCharset()); + assertThat(fileNameGenerator).isInstanceOf(DefaultFileNameGenerator.class); + assertThat(zipResultType).isEqualTo(ZipResultType.BYTE_ARRAY); + assertThat(workDirectory) + .isEqualTo(new File(System.getProperty("java.io.tmpdir") + File.separator + "ziptransformer")); + assertThat(workDirectory.exists()).isTrue(); + assertThat(workDirectory.isDirectory()).isTrue(); + assertThat(compressionLevel).isEqualTo(2); + assertThat(deleteFiles).isTrue(); + } + + @Test + public void testZipTransformerParserWithIncorrectResultType() { + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> + new ClassPathXmlApplicationContext("ZipTransformerParserTestsWithIncorrectResultType.xml", + getClass())) + .withMessageContaining("Failed to convert property value of type 'java.lang.String' " + + "to required type 'org.springframework.integration.zip.transformer.ZipResultType'"); + } + +} diff --git a/spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/ZipTransformerParserTestsWithIncorrectResultType.xml b/spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/ZipTransformerParserTestsWithIncorrectResultType.xml new file mode 100644 index 0000000000..64aa9f2471 --- /dev/null +++ b/spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/ZipTransformerParserTestsWithIncorrectResultType.xml @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/spring-integration-zip/src/test/java/org/springframework/integration/zip/splitter/UnZipResultSplitterTests-context.xml b/spring-integration-zip/src/test/java/org/springframework/integration/zip/splitter/UnZipResultSplitterTests-context.xml new file mode 100644 index 0000000000..4791d6e21c --- /dev/null +++ b/spring-integration-zip/src/test/java/org/springframework/integration/zip/splitter/UnZipResultSplitterTests-context.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + diff --git a/spring-integration-zip/src/test/java/org/springframework/integration/zip/splitter/UnZipResultSplitterTests.java b/spring-integration-zip/src/test/java/org/springframework/integration/zip/splitter/UnZipResultSplitterTests.java new file mode 100644 index 0000000000..00abb01d9a --- /dev/null +++ b/spring-integration-zip/src/test/java/org/springframework/integration/zip/splitter/UnZipResultSplitterTests.java @@ -0,0 +1,133 @@ +/* + * Copyright 2016-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.zip.splitter; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.integration.channel.QueueChannel; +import org.springframework.integration.file.FileHeaders; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.integration.zip.ZipHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Andriy Kryvtsun + * @author Artem Bilan + * + * @since 6.1 + */ +@SpringJUnitConfig +@DirtiesContext +public class UnZipResultSplitterTests { + + private static final String DIR_1 = "dir1/"; + + private static final String DIR_2 = "dir2/"; + + private static final String FILE_1 = "file1"; + + private static final String FILE_2 = "file2"; + + private static final String DATA_1 = "data1"; + + private static final String DATA_2 = "data2"; + + private static final int TIMEOUT = 10000; + + @Autowired + private MessageChannel input; + + @Autowired + private QueueChannel output; + + @Test + public void splitPreservingSourceMessageHeaderValues() { + + final String headerName = "headerName"; + final String headerValue = "headerValue"; + + Message inMessage = MessageBuilder.withPayload(createPayload()) + .setHeader(headerName, headerValue) + .build(); + + input.send(inMessage); + + Message message1 = output.receive(TIMEOUT); + checkMessageWithHeaderValue(message1, headerName, headerValue, DATA_1); + + Message message2 = output.receive(TIMEOUT); + checkMessageWithHeaderValue(message2, headerName, headerValue, DATA_2); + } + + private static void checkMessageWithHeaderValue(Message message, String headerName, String headerValue, + String payload) { + + assertThat(message).isNotNull(); + checkHeaderValue(message, headerName, headerValue); + checkPayload(message, payload); + } + + @Test + public void splitPreservingServiceHeaderValues() { + Message inMessage = MessageBuilder.withPayload(createPayload()) + .setHeader(ZipHeaders.ZIP_ENTRY_PATH, "dir") + .setHeader(FileHeaders.FILENAME, "filename") + .build(); + + input.send(inMessage); + + Message message1 = output.receive(TIMEOUT); + checkMessageWithServiceHeaderValues(message1, DIR_1, FILE_1, DATA_1); + + Message message2 = output.receive(TIMEOUT); + checkMessageWithServiceHeaderValues(message2, DIR_2, FILE_2, DATA_2); + } + + private static void checkMessageWithServiceHeaderValues(Message message, String path, String filename, + String payload) { + + assertThat(message).isNotNull(); + checkHeaderValue(message, ZipHeaders.ZIP_ENTRY_PATH, path); + checkHeaderValue(message, FileHeaders.FILENAME, filename); + checkPayload(message, payload); + } + + private static Map createPayload() { + Map payload = new LinkedHashMap<>(); + payload.put(DIR_1 + FILE_1, DATA_1); + payload.put(DIR_2 + FILE_2, DATA_2); + return payload; + } + + private static void checkPayload(Message message, String payload) { + assertThat(message.getPayload()).isEqualTo(payload); + } + + private static void checkHeaderValue(Message message, String headerName, String headerValue) { + assertThat(message.getHeaders().get(headerName)).isEqualTo(headerValue); + } + +} diff --git a/spring-integration-zip/src/test/java/org/springframework/integration/zip/transformer/UnZipTransformerTests.java b/spring-integration-zip/src/test/java/org/springframework/integration/zip/transformer/UnZipTransformerTests.java new file mode 100644 index 0000000000..ce4a955f26 --- /dev/null +++ b/spring-integration-zip/src/test/java/org/springframework/integration/zip/transformer/UnZipTransformerTests.java @@ -0,0 +1,247 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.zip.transformer; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.Map; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.zeroturnaround.zip.ZipException; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.integration.transformer.MessageTransformationException; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessagingException; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * + * @author Gunnar Hillert + * @author Artem Bilan + * @author Ingo Dueppe + * + * @since 6.1 + */ +@SpringJUnitConfig +@DirtiesContext +public class UnZipTransformerTests { + + @TempDir + public File workDir; + + @Autowired + private ResourceLoader resourceLoader; + + @Test + public void unzipFlatFileEntryZip() throws IOException { + final Resource zipResource = this.resourceLoader.getResource("classpath:testzipdata/flatfileentry.zip"); + final InputStream is = zipResource.getInputStream(); + + final Message message = MessageBuilder.withPayload(is).build(); + + final UnZipTransformer unZipTransformer = new UnZipTransformer(); + unZipTransformer.setZipResultType(ZipResultType.FILE); + unZipTransformer.afterPropertiesSet(); + + final Message resultMessage = unZipTransformer.transform(message); + + assertThat(resultMessage).isNotNull(); + + @SuppressWarnings("unchecked") + Map unzippedData = (Map) resultMessage.getPayload(); + + assertThat(unzippedData).isNotNull().hasSize(1); + } + + @Test + public void unzipSingleFileAsInputStreamToByteArray() throws IOException { + final Resource resource = this.resourceLoader.getResource("classpath:testzipdata/single.zip"); + final InputStream is = resource.getInputStream(); + + final Message message = MessageBuilder.withPayload(is).build(); + + final UnZipTransformer unZipTransformer = new UnZipTransformer(); + unZipTransformer.setZipResultType(ZipResultType.BYTE_ARRAY); + unZipTransformer.afterPropertiesSet(); + + final Message resultMessage = unZipTransformer.transform(message); + + assertThat(resultMessage).isNotNull(); + + @SuppressWarnings("unchecked") + Map unzippedData = (Map) resultMessage.getPayload(); + + assertThat(unzippedData).isNotNull().hasSize(1); + assertThat(new String(unzippedData.values().iterator().next())).isEqualTo("Spring Integration Rocks!"); + } + + @Test + public void unzipSingleFileToByteArray() throws IOException { + final Resource resource = this.resourceLoader.getResource("classpath:testzipdata/single.zip"); + final InputStream is = resource.getInputStream(); + + final File inputFile = new File(this.workDir, "unzipSingleFileToByteArray"); + + FileOutputStream out = new FileOutputStream(inputFile); + IOUtils.copy(is, out); + is.close(); + out.close(); + + final Message message = MessageBuilder.withPayload(inputFile).build(); + + final UnZipTransformer unZipTransformer = new UnZipTransformer(); + unZipTransformer.setZipResultType(ZipResultType.BYTE_ARRAY); + unZipTransformer.afterPropertiesSet(); + + final Message resultMessage = unZipTransformer.transform(message); + + assertThat(resultMessage).isNotNull(); + + @SuppressWarnings("unchecked") + Map unzippedData = (Map) resultMessage.getPayload(); + + assertThat(unzippedData).isNotNull().hasSize(1); + assertThat(inputFile).exists(); + assertThat(new String(unzippedData.values().iterator().next())).isEqualTo("Spring Integration Rocks!"); + } + + @Test + public void unzipSingleFileToByteArrayWithDeleteFilesTrue() throws IOException { + final Resource resource = this.resourceLoader.getResource("classpath:testzipdata/single.zip"); + final InputStream is = resource.getInputStream(); + + final File inputFile = new File(this.workDir, "unzipSingleFileToByteArray"); + + FileOutputStream output = new FileOutputStream(inputFile); + IOUtils.copy(is, output); + is.close(); + output.close(); + + final Message message = MessageBuilder.withPayload(inputFile).build(); + + final UnZipTransformer unZipTransformer = new UnZipTransformer(); + unZipTransformer.setZipResultType(ZipResultType.BYTE_ARRAY); + unZipTransformer.setDeleteFiles(true); + unZipTransformer.afterPropertiesSet(); + + final Message resultMessage = unZipTransformer.transform(message); + + assertThat(resultMessage).isNotNull(); + + @SuppressWarnings("unchecked") + Map unzippedData = (Map) resultMessage.getPayload(); + + assertThat(unzippedData).isNotNull().hasSize(1); + assertThat(inputFile).doesNotExist(); + assertThat(new String(unzippedData.values().iterator().next())).isEqualTo("Spring Integration Rocks!"); + } + + @Test + public void unzipMultipleFilesAsInputStreamToByteArray() throws IOException { + final Resource resource = this.resourceLoader.getResource("classpath:testzipdata/countries.zip"); + final InputStream is = resource.getInputStream(); + + final Message message = MessageBuilder.withPayload(is).build(); + + final UnZipTransformer unZipTransformer = new UnZipTransformer(); + unZipTransformer.setZipResultType(ZipResultType.BYTE_ARRAY); + unZipTransformer.afterPropertiesSet(); + + final Message resultMessage = unZipTransformer.transform(message); + + assertThat(resultMessage).isNotNull(); + + @SuppressWarnings("unchecked") + Map unzippedData = (Map) resultMessage.getPayload(); + + assertThat(unzippedData).isNotNull().hasSize(5); + } + + @Test + public void unzipMultipleFilesAsInputStreamWithExpectSingleResultTrue() throws IOException { + final Resource resource = this.resourceLoader.getResource("classpath:testzipdata/countries.zip"); + final InputStream is = resource.getInputStream(); + + final Message message = MessageBuilder.withPayload(is).build(); + + final UnZipTransformer unZipTransformer = new UnZipTransformer(); + unZipTransformer.setZipResultType(ZipResultType.BYTE_ARRAY); + unZipTransformer.setExpectSingleResult(true); + unZipTransformer.afterPropertiesSet(); + + + assertThatExceptionOfType(MessagingException.class) + .isThrownBy(() -> unZipTransformer.transform(message)) + .withStackTraceContaining("The UnZip operation extracted 5 result objects " + + "but expectSingleResult was 'true'."); + } + + @Test + public void unzipInvalidZipFile() throws IOException { + File fileToUnzip = File.createTempFile("test1", "tmp"); + FileUtils.writeStringToFile(fileToUnzip, "hello world", Charset.defaultCharset()); + + UnZipTransformer unZipTransformer = new UnZipTransformer(); + unZipTransformer.setZipResultType(ZipResultType.BYTE_ARRAY); + unZipTransformer.setExpectSingleResult(true); + unZipTransformer.afterPropertiesSet(); + + Message message = MessageBuilder.withPayload(fileToUnzip).build(); + + assertThatExceptionOfType(MessagingException.class) + .isThrownBy(() -> unZipTransformer.transform(message)) + .withStackTraceContaining(String.format("Not a zip file: %s", fileToUnzip.getAbsolutePath())); + } + + @Test + public void testUnzipMaliciousTraversalZipFile() throws IOException { + final Resource resource = this.resourceLoader.getResource("classpath:testzipdata/zip-malicious-traversal.zip"); + final InputStream is = resource.getInputStream(); + + final Message message = MessageBuilder.withPayload(is).build(); + + final UnZipTransformer unZipTransformer = new UnZipTransformer(); + unZipTransformer.afterPropertiesSet(); + + + assertThatExceptionOfType(MessageTransformationException.class) + .isThrownBy(() -> unZipTransformer.transform(message)) + .withRootCauseInstanceOf(ZipException.class) + .withStackTraceContaining("is trying to leave the target output directory"); + } + + @Configuration + public static class TestConfiguration { + + } + +} diff --git a/spring-integration-zip/src/test/java/org/springframework/integration/zip/transformer/ZipTransformerTests.java b/spring-integration-zip/src/test/java/org/springframework/integration/zip/transformer/ZipTransformerTests.java new file mode 100644 index 0000000000..54a4a9bada --- /dev/null +++ b/spring-integration-zip/src/test/java/org/springframework/integration/zip/transformer/ZipTransformerTests.java @@ -0,0 +1,246 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.zip.transformer; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.RandomAccessFile; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.zeroturnaround.zip.ZipUtil; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.integration.zip.ZipHeaders; +import org.springframework.messaging.Message; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * + * @author Gunnar Hillert + * @author Artem Bilan + * + * @since 6.1 + */ +public class ZipTransformerTests { + + @TempDir + public File workDir; + + @Test + public void zipString() { + final ZipTransformer zipTransformer = new ZipTransformer(); + zipTransformer.setBeanFactory(mock(BeanFactory.class)); + zipTransformer.setZipResultType(ZipResultType.BYTE_ARRAY); + zipTransformer.afterPropertiesSet(); + + final String stringToCompress = "Hello World"; + + final Date fileDate = new Date(); + + final Message message = MessageBuilder.withPayload(stringToCompress) + .setHeader(ZipHeaders.ZIP_ENTRY_FILE_NAME, "test.txt") + .setHeader(ZipHeaders.ZIP_ENTRY_LAST_MODIFIED_DATE, fileDate) + .build(); + + final Message result = zipTransformer.transform(message); + + Object resultPayload = result.getPayload(); + + assertThat(resultPayload).isInstanceOf(byte[].class); + + ZipUtil.unpack(new ByteArrayInputStream((byte[]) resultPayload), this.workDir); + + final File unzippedEntry = new File(this.workDir, "test.txt"); + assertThat(unzippedEntry).exists().isFile(); + + //See https://stackoverflow.com/questions/3725662/what-is-the-earliest-timestamp-value-that-is-supported-in-zip-file-format + assertThat(unzippedEntry.lastModified()) + .isGreaterThan(fileDate.getTime() - 3000) + .isLessThan(fileDate.getTime() + 3000); + } + + @Test + public void zipStringCollection() { + final ZipTransformer zipTransformer = new ZipTransformer(); + zipTransformer.setBeanFactory(mock(BeanFactory.class)); + zipTransformer.setZipResultType(ZipResultType.BYTE_ARRAY); + zipTransformer.afterPropertiesSet(); + + final String string1ToCompress = "Cartman"; + final String string2ToCompress = "Kenny"; + final String string3ToCompress = "Butters"; + + final List strings = new ArrayList<>(3); + + strings.add(string1ToCompress); + strings.add(string2ToCompress); + strings.add(string3ToCompress); + + final Date fileDate = new Date(); + + final Message> message = MessageBuilder.withPayload(strings) + .setHeader(ZipHeaders.ZIP_ENTRY_FILE_NAME, "test.txt") + .setHeader(ZipHeaders.ZIP_ENTRY_LAST_MODIFIED_DATE, fileDate) + .build(); + + final Message result = zipTransformer.transform(message); + + Object resultPayload = result.getPayload(); + + assertThat(resultPayload).isInstanceOf(byte[].class); + + ZipUtil.unpack(new ByteArrayInputStream((byte[]) resultPayload), this.workDir); + + File[] files = this.workDir.listFiles(); + + assertThat(files).hasSizeGreaterThanOrEqualTo(3); + + final Set expectedFileNames = new HashSet<>(); + + expectedFileNames.add("test_1.txt"); + expectedFileNames.add("test_2.txt"); + expectedFileNames.add("test_3.txt"); + + for (File file : files) { + if (file.getName().startsWith("test")) { + assertThat(file).exists().isFile(); + + //See https://stackoverflow.com/questions/3725662/what-is-the-earliest-timestamp-value-that-is-supported-in-zip-file-format + assertThat(file.lastModified()) + .isLessThan(fileDate.getTime() + 4000) + .isGreaterThan(fileDate.getTime() - 4000); + + assertThat(file).hasExtension("txt"); + + assertThat(expectedFileNames).contains(file.getName()); + } + } + } + + @Test + public void zipStringToFile() { + final ZipTransformer zipTransformer = new ZipTransformer(); + zipTransformer.setBeanFactory(mock(BeanFactory.class)); + zipTransformer.afterPropertiesSet(); + + final String stringToCompress = "Hello World"; + + final String zipEntryFileName = "test.txt"; + final Message message = MessageBuilder.withPayload(stringToCompress) + .setHeader(ZipHeaders.ZIP_ENTRY_FILE_NAME, zipEntryFileName) + .build(); + + final Message result = zipTransformer.transform(message); + + assertThat(result.getPayload()).isInstanceOf(File.class); + + final File payload = (File) result.getPayload(); + + assertThat(payload).hasName(message.getHeaders().getId().toString() + ".msg.zip"); + assertThat((SpringZipUtils.isValid(payload))).isTrue(); + + final byte[] zipEntryData = ZipUtil.unpackEntry(payload, "test.txt"); + + assertThat(zipEntryData).isNotNull(); + assertThat(new String(zipEntryData)).isEqualTo("Hello World"); + } + + @Test + public void zipFile() { + + ZipTransformer zipTransformer = new ZipTransformer(); + zipTransformer.setBeanFactory(mock(BeanFactory.class)); + zipTransformer.setDeleteFiles(true); + zipTransformer.afterPropertiesSet(); + + final File testFile = createTestFile(10); + + assertThat(testFile).exists(); + + final Message message = MessageBuilder.withPayload(testFile).build(); + + final Message result = zipTransformer.transform(message); + + assertThat(result.getPayload()).isInstanceOf(File.class); + + final File payload = (File) result.getPayload(); + + assertThat(payload).hasName(testFile.getName() + ".zip"); + assertThat(SpringZipUtils.isValid(payload)).isTrue(); + } + + @Test + public void zipCollection() { + + final File testFile1 = createTestFile(1); + final File testFile2 = createTestFile(2); + final File testFile3 = createTestFile(3); + final File testFile4 = createTestFile(4); + + assertThat(testFile1).exists(); + assertThat(testFile2).exists(); + assertThat(testFile3).exists(); + assertThat(testFile4).exists(); + + final Collection files = new ArrayList<>(); + + files.add(testFile1); + files.add(testFile2); + files.add(testFile3); + files.add(testFile4); + + final ZipTransformer zipTransformer = new ZipTransformer(); + zipTransformer.setBeanFactory(mock(BeanFactory.class)); + zipTransformer.afterPropertiesSet(); + + final Message> message = MessageBuilder.withPayload(files).build(); + + final Message result = zipTransformer.transform(message); + + assertThat(result.getPayload()).isInstanceOf(File.class); + + final File outputZipFile = (File) result.getPayload(); + + assertThat(outputZipFile).exists().isFile().hasExtension("zip"); + assertThat(SpringZipUtils.isValid(outputZipFile)).isTrue(); + } + + private File createTestFile(int size) { + final File testFile = new File(this.workDir, "testdata" + UUID.randomUUID().toString() + ".data"); + + try (RandomAccessFile f = new RandomAccessFile(testFile, "rw")) { + f.setLength((long) size * 1024 * 1024); + } + catch (Exception e) { + // Ignore + } + return testFile; + + } + +} diff --git a/spring-integration-zip/src/test/resources/log4j2-test.xml b/spring-integration-zip/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000000..cb3f4a0658 --- /dev/null +++ b/spring-integration-zip/src/test/resources/log4j2-test.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/spring-integration-zip/src/test/resources/testzipdata/countries.zip b/spring-integration-zip/src/test/resources/testzipdata/countries.zip new file mode 100644 index 0000000000..89e7e4d9a9 Binary files /dev/null and b/spring-integration-zip/src/test/resources/testzipdata/countries.zip differ diff --git a/spring-integration-zip/src/test/resources/testzipdata/countries/continents/asia.txt b/spring-integration-zip/src/test/resources/testzipdata/countries/continents/asia.txt new file mode 100644 index 0000000000..a287965241 --- /dev/null +++ b/spring-integration-zip/src/test/resources/testzipdata/countries/continents/asia.txt @@ -0,0 +1 @@ +Asia \ No newline at end of file diff --git a/spring-integration-zip/src/test/resources/testzipdata/countries/continents/europe.txt b/spring-integration-zip/src/test/resources/testzipdata/countries/continents/europe.txt new file mode 100644 index 0000000000..c6af3c9174 --- /dev/null +++ b/spring-integration-zip/src/test/resources/testzipdata/countries/continents/europe.txt @@ -0,0 +1 @@ +Europe \ No newline at end of file diff --git a/spring-integration-zip/src/test/resources/testzipdata/countries/de.txt b/spring-integration-zip/src/test/resources/testzipdata/countries/de.txt new file mode 100644 index 0000000000..646bce2966 --- /dev/null +++ b/spring-integration-zip/src/test/resources/testzipdata/countries/de.txt @@ -0,0 +1 @@ +Germany \ No newline at end of file diff --git a/spring-integration-zip/src/test/resources/testzipdata/countries/fr.txt b/spring-integration-zip/src/test/resources/testzipdata/countries/fr.txt new file mode 100644 index 0000000000..b9a7c52fb6 --- /dev/null +++ b/spring-integration-zip/src/test/resources/testzipdata/countries/fr.txt @@ -0,0 +1 @@ +France \ No newline at end of file diff --git a/spring-integration-zip/src/test/resources/testzipdata/countries/pl.txt b/spring-integration-zip/src/test/resources/testzipdata/countries/pl.txt new file mode 100644 index 0000000000..639acf6a06 --- /dev/null +++ b/spring-integration-zip/src/test/resources/testzipdata/countries/pl.txt @@ -0,0 +1 @@ +Poland \ No newline at end of file diff --git a/spring-integration-zip/src/test/resources/testzipdata/flat.txt b/spring-integration-zip/src/test/resources/testzipdata/flat.txt new file mode 100644 index 0000000000..8758da1ca9 --- /dev/null +++ b/spring-integration-zip/src/test/resources/testzipdata/flat.txt @@ -0,0 +1 @@ +Spring Integration Rocks! \ No newline at end of file diff --git a/spring-integration-zip/src/test/resources/testzipdata/flatfileentry.zip b/spring-integration-zip/src/test/resources/testzipdata/flatfileentry.zip new file mode 100644 index 0000000000..22d2d829e9 Binary files /dev/null and b/spring-integration-zip/src/test/resources/testzipdata/flatfileentry.zip differ diff --git a/spring-integration-zip/src/test/resources/testzipdata/single.txt b/spring-integration-zip/src/test/resources/testzipdata/single.txt new file mode 100644 index 0000000000..8758da1ca9 --- /dev/null +++ b/spring-integration-zip/src/test/resources/testzipdata/single.txt @@ -0,0 +1 @@ +Spring Integration Rocks! \ No newline at end of file diff --git a/spring-integration-zip/src/test/resources/testzipdata/single.zip b/spring-integration-zip/src/test/resources/testzipdata/single.zip new file mode 100644 index 0000000000..9f61c70a8c Binary files /dev/null and b/spring-integration-zip/src/test/resources/testzipdata/single.zip differ diff --git a/spring-integration-zip/src/test/resources/testzipdata/zip-malicious-traversal.zip b/spring-integration-zip/src/test/resources/testzipdata/zip-malicious-traversal.zip new file mode 100644 index 0000000000..38b3f499de Binary files /dev/null and b/spring-integration-zip/src/test/resources/testzipdata/zip-malicious-traversal.zip differ diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 19756a9ebc..d26add490a 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -17,6 +17,11 @@ In general the project has been moved to the latest dependency versions. [[x6.1-new-components]] === New Components +[[x6.1-zip]] +==== Zip Support + +The Zip Spring Integration Extension project has been migrated as the `spring-integration-zip` module. +See <<./zip.adoc#zip,Zip Support>> for more information. [[x6.1-general]] diff --git a/src/reference/asciidoc/zip.adoc b/src/reference/asciidoc/zip.adoc new file mode 100644 index 0000000000..7b475a38b8 --- /dev/null +++ b/src/reference/asciidoc/zip.adoc @@ -0,0 +1,272 @@ +[[zip]] +== Zip Support + +This Spring Integration module provides https://en.wikipedia.org/wiki/ZIP_(file_format)[Zip] (un-)compression support. +A zipping algorithm implementation is based on a https://github.com/zeroturnaround/zt-zip[ZeroTurnaround ZIP Library]. +The following components are provided: + +* <> +* <> +* <> + +You need to include this dependency into your project: + +==== +[source, xml, subs="normal", role="primary"] +.Maven +---- + + org.springframework.integration + spring-integration-zip + {project-version} + +---- + +[source, groovy, subs="normal", role="secondary"] +.Gradle +---- +compile "org.springframework.integration:spring-integration-zip:{project-version}" +---- +==== + +[[xpath-namespace-support]] +=== Namespace Support + +All components within the Spring Integration Zip module provide namespace support. +In order to enable namespace support, you need to import the schema for the Spring Integration Zip Module. +The following example shows a typical setup: + +==== +[source,xml] +---- + + + +---- +==== + +[[zip-transformer]] +=== (Un)Zip Transformer + +The `ZipTransformer` implements a zipping functionality for these types of input `payload`: `File`, `String`, `byte[]` and `Iterable`. +In input data types can be mixed as part of an `Iterable`. +For example, it should be easily to compress a collection containing Strings, byte arrays and Files. +It is important to note that nested Iterables are *NOT SUPPORTED* at present time. + +The `ZipTransformer` can be customized by setting several properties: + +* `compressionLevel` - sets the compression level. +Default is `Deflater#DEFAULT_COMPRESSION`. + +* `useFileAttributes` - specifies whether the name of the file shall be used for the zip entry. + +For example to zip a simple `test.txt` file into a `test.txt.zip`, only this configuration is enough: + +==== +[source, java, role="primary"] +.Java DSL +---- +@Bean +public IntegrationFlow zipFlow() { + return IntegrationFlow + .from("zipChannel") + .transform(new ZipTransformer()) + .get(); +} +---- +[source, kotlin, role="secondary"] +.Kotlin DSL +---- +@Bean +fun zipFlow() = + integrationFlow("zipChannel") { + transform(ZipTransformer()) + } +---- +[source, groovy, role="secondary"] +.Groovy DSL +---- +@Bean +zipFlow() { + integrationFlow 'zipChannel', + { + transform new ZipTransformer() + } +} +---- +[source, java, role="secondary"] +.Java +---- +@Transfomer(inputChannel = "zipChannel") +@Bean +ZipTransformer zipTransformer() { + return new ZipTransformer(); +} +---- +[source, xml, role="secondary"] +.XML +---- + +---- +==== + +See `ZipTransformer` Javadocs for more information. + +An `UnZipTransformer` supports these of input `payload`: `File`, `byte[]` and `InputStream`. +When unzipping data, an `expectSingleResult` property can be specified. +If set to `true` and more than `1` zip entry were detected, a `MessagingException` will be raised. +This property also influences the return type of the payload. +If set to `false` (default), then the payload will be of type `SortedMap`, if `true`, however, the actual zip entry will be returned. + +Other properties that can be set on the `UnZipTransformer`: + +* `deleteFiles` - if the payload is an instance of `File`, this property specifies whether to delete the File after transformation. +Default is `false`. + +* `ZipResultType` - defines the format of the data returned after transformation. +Available options are: `File`, `byte[]`. + +* `workDirectory` - the work directory is used when a `ZipResultType` is set to `ZipResultType.FILE`. +By default, this property is set to the System temporary directory containing a subdirectory `ziptransformer`. + +For example to zip a simple `test.zip` file into a map of its entries, only this configuration is enough: + +==== +[source, java, role="primary"] +.Java DSL +---- +@Bean +public IntegrationFlow unzipFlow() { + return IntegrationFlow + .from("unzipChannel") + .transform(new UnZipTransformer()) + .get(); +} +---- +[source, kotlin, role="secondary"] +.Kotlin DSL +---- +@Bean +fun unzipFlow() = + integrationFlow("unzipChannel") { + transform(UnZipTransformer()) + } +---- +[source, groovy, role="secondary"] +.Groovy DSL +---- +@Bean +unzipFlow() { + integrationFlow 'unzipChannel', + { + transform new UnZipTransformer() + } +} +---- +[source, java, role="secondary"] +.Java +---- +@Transfomer(inputChannel = "unzipChannel") +@Bean +UnZipTransformer unzipTransformer() { + return new UnZipTransformer(); +} +---- +[source, xml, role="secondary"] +.XML +---- + +---- +==== + +[[unzip-splitter]] +=== Unzipped Splitter + +The `UnZipResultSplitter` is useful in cases where zip files contain more than `1` entry. +Essentially it has to be used as the next step in the integration flow after the mentioned above `UnZipTransformer`. +It supports only a `Map` as an input data and emits every entry into an `outputChannel` with `FileHeaders.FILENAME` and `ZipHeaders.ZIP_ENTRY_PATH` headers. + +The following example demonstrates a simple configuration for splitting unzipped result: + +==== +[source, java, role="primary"] +.Java DSL +---- +@Bean +public IntegrationFlow unzipSplitFlow(Executor executor) { + return IntegrationFlow + .from("unzipChannel") + .transform(new UnZipTransformer()) + .split(new UnZipResultSplitter()) + .channel(c -> c.executor("entriesChannel", executor)) + .get(); +} +---- +[source, kotlin, role="secondary"] +.Kotlin DSL +---- +@Bean +fun unzipFlow(executor: Executor) = + integrationFlow("unzipChannel") { + transform(UnZipTransformer()) + split(UnZipResultSplitter()) + channel { executor("entriesChannel", executor) } + } +---- +[source, groovy, role="secondary"] +.Groovy DSL +---- +@Bean +unzipFlow(Executor executor) { + integrationFlow 'unzipChannel', + { + transform new UnZipTransformer() + split new UnZipResultSplitter() + channel { executor 'entriesChannel', executor } + } +} +---- +[source, java, role="secondary"] +.Java +---- +@Transfomer(inputChannel = "unzipChannel", outputChannel = "splitChannel") +@Bean +UnZipTransformer unzipTransformer() { + return new UnZipTransformer(); +} + +@Spitter(inputChannel = "splitChannel", outputChannel = "entriesChannel") +@Bean +UnZipResultSplitter unZipSplitter() { + return new UnZipResultSplitter(); +} + +@Bean +ExecutorChannel entriesChannel(Executor executor) { + return new ExecutorChannel(executor); +} +---- +[source, xml, role="secondary"] +.XML +---- + + + + + + + + + + +---- +====