From 416cc39da4188469bdd508d992250bba4263ffd2 Mon Sep 17 00:00:00 2001 From: Gunnar Hillert Date: Mon, 15 Jun 2015 17:40:00 -0500 Subject: [PATCH] INTEXT-40 Add ZIP Transformer * Add zip-transformer * Add unzip-transformer * Add UnZipResultSplitter * Add sample For reference see: https://jira.springsource.org/browse/INTEXT-40 --- .gitignore | 2 + README.md | 2 +- samples/zip/README.md | 56 ++ samples/zip/pom.xml | 119 ++++ .../integration/samples/zip/Main.java | 84 +++ .../samples/zip/SpringIntegrationUtils.java | 91 +++ .../samples/zip/TransformationHandler.java | 70 ++ .../spring-integration-context.xml | 39 ++ samples/zip/src/main/resources/log4j.xml | 28 + .../SpringIntegrationProjectStartupTest.java | 43 ++ .../support/ConsumerConfigurationTests.java | 610 ++++++++++++++++++ spring-integration-zip/README.md | 171 +++++ spring-integration-zip/build.gradle | 243 +++++++ spring-integration-zip/gradle.properties | 1 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 51017 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + spring-integration-zip/gradlew | 164 +++++ spring-integration-zip/gradlew.bat | 90 +++ spring-integration-zip/publish-maven.gradle | 61 ++ spring-integration-zip/src/api/overview.html | 22 + spring-integration-zip/src/dist/changelog.txt | 15 + spring-integration-zip/src/dist/license.txt | 201 ++++++ spring-integration-zip/src/dist/notice.txt | 21 + spring-integration-zip/src/dist/readme.txt | 13 + .../integration/zip/ZipHeaders.java | 37 ++ .../xml/AbstractZipTransformerParser.java | 58 ++ .../config/xml/UnZipTransformerParser.java | 65 ++ .../zip/config/xml/ZipNamespaceHandler.java | 38 ++ .../zip/config/xml/ZipTransformerParser.java | 65 ++ .../zip/config/xml/package-info.java | 4 + .../integration/zip/package-info.java | 4 + .../transformer/AbstractZipTransformer.java | 131 ++++ .../zip/transformer/SpringZipUtils.java | 149 +++++ .../zip/transformer/UnZipTransformer.java | 190 ++++++ .../zip/transformer/ZipResultType.java | 62 ++ .../zip/transformer/ZipTransformer.java | 229 +++++++ .../splitter/UnZipResultSplitter.java | 52 ++ .../main/resources/META-INF/spring.handlers | 1 + .../main/resources/META-INF/spring.schemas | 2 + .../main/resources/META-INF/spring.tooling | 4 + .../config/xml/spring-integration-zip-1.0.xsd | 148 +++++ .../config/xml/spring-integration-zip.gif | Bin 0 -> 539 bytes .../zip/UnZip2FileTests-context.xml | 34 + .../integration/zip/UnZip2FileTests.java | 160 +++++ .../integration/zip/Zip2FileTests-context.xml | 21 + .../integration/zip/Zip2FileTests.java | 175 +++++ .../xml/UnZipTransformerParserTests.java | 140 ++++ .../xml/UnZipTransformerParserTests.xml | 19 + .../config/xml/ZipTransformerParserTests.java | 159 +++++ .../config/xml/ZipTransformerParserTests.xml | 19 + ...rmerParserTestsWithIncorrectResultType.xml | 17 + .../transformer/UnZipTransformerTests.java | 253 ++++++++ .../zip/transformer/UnZipTransformerTests.xml | 10 + .../zip/transformer/ZipTransformerTests.java | 284 ++++++++ .../src/test/resources/log4j.properties | 8 + .../test/resources/testzipdata/countries.zip | Bin 0 -> 657 bytes .../testzipdata/countries/continents/asia.txt | 1 + .../countries/continents/europe.txt | 1 + .../resources/testzipdata/countries/de.txt | 1 + .../resources/testzipdata/countries/fr.txt | 1 + .../resources/testzipdata/countries/pl.txt | 1 + .../src/test/resources/testzipdata/single.zip | Bin 0 -> 145 bytes 62 files changed, 4694 insertions(+), 1 deletion(-) create mode 100644 samples/zip/README.md create mode 100644 samples/zip/pom.xml create mode 100644 samples/zip/src/main/java/org/springframework/integration/samples/zip/Main.java create mode 100644 samples/zip/src/main/java/org/springframework/integration/samples/zip/SpringIntegrationUtils.java create mode 100644 samples/zip/src/main/java/org/springframework/integration/samples/zip/TransformationHandler.java create mode 100644 samples/zip/src/main/resources/META-INF/spring/integration/spring-integration-context.xml create mode 100644 samples/zip/src/main/resources/log4j.xml create mode 100644 samples/zip/src/test/java/org/springframework/integration/samples/zip/SpringIntegrationProjectStartupTest.java create mode 100644 spring-integration-kafka/src/test/java/org/springframework/integration/kafka/support/ConsumerConfigurationTests.java create mode 100644 spring-integration-zip/README.md create mode 100644 spring-integration-zip/build.gradle create mode 100644 spring-integration-zip/gradle.properties create mode 100644 spring-integration-zip/gradle/wrapper/gradle-wrapper.jar create mode 100644 spring-integration-zip/gradle/wrapper/gradle-wrapper.properties create mode 100755 spring-integration-zip/gradlew create mode 100644 spring-integration-zip/gradlew.bat create mode 100644 spring-integration-zip/publish-maven.gradle create mode 100644 spring-integration-zip/src/api/overview.html create mode 100644 spring-integration-zip/src/dist/changelog.txt create mode 100644 spring-integration-zip/src/dist/license.txt create mode 100644 spring-integration-zip/src/dist/notice.txt create mode 100644 spring-integration-zip/src/dist/readme.txt create mode 100644 spring-integration-zip/src/main/java/org/springframework/integration/zip/ZipHeaders.java create mode 100644 spring-integration-zip/src/main/java/org/springframework/integration/zip/config/xml/AbstractZipTransformerParser.java create mode 100644 spring-integration-zip/src/main/java/org/springframework/integration/zip/config/xml/UnZipTransformerParser.java create mode 100644 spring-integration-zip/src/main/java/org/springframework/integration/zip/config/xml/ZipNamespaceHandler.java create mode 100644 spring-integration-zip/src/main/java/org/springframework/integration/zip/config/xml/ZipTransformerParser.java create mode 100644 spring-integration-zip/src/main/java/org/springframework/integration/zip/config/xml/package-info.java create mode 100644 spring-integration-zip/src/main/java/org/springframework/integration/zip/package-info.java create mode 100644 spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/AbstractZipTransformer.java create mode 100644 spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/SpringZipUtils.java create mode 100644 spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/UnZipTransformer.java create mode 100644 spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/ZipResultType.java create mode 100644 spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/ZipTransformer.java create mode 100644 spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/splitter/UnZipResultSplitter.java create mode 100644 spring-integration-zip/src/main/resources/META-INF/spring.handlers create mode 100644 spring-integration-zip/src/main/resources/META-INF/spring.schemas create mode 100644 spring-integration-zip/src/main/resources/META-INF/spring.tooling create mode 100644 spring-integration-zip/src/main/resources/org/springframework/integration/config/xml/spring-integration-zip-1.0.xsd create mode 100644 spring-integration-zip/src/main/resources/org/springframework/integration/config/xml/spring-integration-zip.gif create mode 100644 spring-integration-zip/src/test/java/org/springframework/integration/zip/UnZip2FileTests-context.xml create mode 100644 spring-integration-zip/src/test/java/org/springframework/integration/zip/UnZip2FileTests.java create mode 100644 spring-integration-zip/src/test/java/org/springframework/integration/zip/Zip2FileTests-context.xml create mode 100644 spring-integration-zip/src/test/java/org/springframework/integration/zip/Zip2FileTests.java create mode 100644 spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/UnZipTransformerParserTests.java create mode 100644 spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/UnZipTransformerParserTests.xml create mode 100644 spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/ZipTransformerParserTests.java create mode 100644 spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/ZipTransformerParserTests.xml create mode 100644 spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/ZipTransformerParserTestsWithIncorrectResultType.xml create mode 100644 spring-integration-zip/src/test/java/org/springframework/integration/zip/transformer/UnZipTransformerTests.java create mode 100644 spring-integration-zip/src/test/java/org/springframework/integration/zip/transformer/UnZipTransformerTests.xml create mode 100644 spring-integration-zip/src/test/java/org/springframework/integration/zip/transformer/ZipTransformerTests.java create mode 100644 spring-integration-zip/src/test/resources/log4j.properties create mode 100644 spring-integration-zip/src/test/resources/testzipdata/countries.zip create mode 100644 spring-integration-zip/src/test/resources/testzipdata/countries/continents/asia.txt create mode 100644 spring-integration-zip/src/test/resources/testzipdata/countries/continents/europe.txt create mode 100644 spring-integration-zip/src/test/resources/testzipdata/countries/de.txt create mode 100644 spring-integration-zip/src/test/resources/testzipdata/countries/fr.txt create mode 100644 spring-integration-zip/src/test/resources/testzipdata/countries/pl.txt create mode 100644 spring-integration-zip/src/test/resources/testzipdata/single.zip diff --git a/.gitignore b/.gitignore index 6c4962e..6a5b018 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ target spring-integration-aws/src/test/resources/awscredentials.properties *.orig /spring-integration-java-dsl/hostkey.ser +.springBeans + diff --git a/README.md b/README.md index 865987f..cc1c907 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ The Spring Integration Extensions project provides extension modules for [Spring * [Splunk][] Support * [Voldemort][] Support * [XQuery][] Support - +* Zip Support (Compression and Uncompression) ## Samples diff --git a/samples/zip/README.md b/samples/zip/README.md new file mode 100644 index 0000000..cf5fd47 --- /dev/null +++ b/samples/zip/README.md @@ -0,0 +1,56 @@ +Spring Integration Zip Sample +=================== + +This sample illustrates the usage of the Spring Integration Zip Extension. It uses the following components: + +* zip-transformer +* unzip-transformer + +You can run the application by either + +* running the "Main" class from within STS (Right-click on Main class --> Run As --> Java Application) +* or from the command line: + - mvn package + - mvn exec:java + +You should see a screen as the following: + +``` +========================================================= + + Welcome to the Spring Integration Zip Sample + + For more information please visit: + http://www.springsource.org/spring-integration + +========================================================= +17:08:41.883 INFO [org.springframework.integration.samples.zip.Main.main()][org.springframework.integration.samples.zip.SpringIntegrationUtils] +========================================================= + + Intput directory is: '/dev/spring-integration-extensions/samples/zip/input-zip' + Intput directory is: '/dev/spring-integration-extensions/samples/zip/input-uncompressed' + Output directory is: 'target/output/decompressedFilesOut' + Output directory is: 'target/output/zipFilesOut' + +========================================================= +17:08:41.887 INFO [org.springframework.integration.samples.zip.Main.main()][org.springframework.integration.samples.zip.Main] +========================================================= + + Please press 'q + Enter' to quit the application. + +========================================================= +``` +## Compressing Files + +Drop an uncompressed file into the **input-uncompressed** directory. The file will be compressed and stored under **target/output/zipFilesOut**. + +## Uncompressing Files + +Drop a compressed file into the **input-zip** directory. The file will be decompressed and stored under **target/output/decompressedFilesOut**. + +-------------------------------------------------------------------------------- + +For help please take a look at the Spring Integration documentation: + +http://www.springsource.org/spring-integration + diff --git a/samples/zip/pom.xml b/samples/zip/pom.xml new file mode 100644 index 0000000..b396dec --- /dev/null +++ b/samples/zip/pom.xml @@ -0,0 +1,119 @@ + + 4.0.0 + + org.springframework.integration.samples + zip-sample + 1.0.0.BUILD-SNAPSHOT + jar + + zip-sample + http://projects.spring.io/spring-integration/ + + + 2.2.1 + + + + UTF-8 + 1.0.0.BUILD-SNAPSHOT + 1.2.17 + 4.11 + + + + + repo.spring.io.milestone + Spring Framework Maven Milestone Repository + https://repo.spring.io/libs-milestone + + + + + + + maven-eclipse-plugin + 2.9 + + + org.springframework.ide.eclipse.core.springnature + + + org.springframework.ide.eclipse.core.springbuilder + + true + true + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.0 + + 1.6 + 1.6 + -Xlint:all + true + true + + + + org.codehaus.mojo + exec-maven-plugin + 1.2.1 + + org.springframework.integration.samples.zip.Main + + + + + + + + + + + junit + junit + ${junit.version} + test + + + + + + org.springframework.integration + spring-integration-zip + ${spring.integration.zip.version} + + + + org.springframework + spring-core + 4.2.0.RC1 + + + + + + log4j + log4j + ${log4j.version} + + + + + + commons-io + commons-io + 2.4 + + + + org.slf4j + slf4j-jcl + 1.7.12 + + + + diff --git a/samples/zip/src/main/java/org/springframework/integration/samples/zip/Main.java b/samples/zip/src/main/java/org/springframework/integration/samples/zip/Main.java new file mode 100644 index 0000000..cbd3e69 --- /dev/null +++ b/samples/zip/src/main/java/org/springframework/integration/samples/zip/Main.java @@ -0,0 +1,84 @@ +/* + * Copyright 2015 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 + * + * http://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.samples.zip; + +import java.util.Scanner; + +import org.apache.log4j.Logger; +import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + + +/** + * Starts the Spring Context and will initialize the Spring Integration routes. + * + * @author Gunnar Hillert + * @since 1.0 + * + */ +public final class Main { + + private static final Logger LOGGER = Logger.getLogger(Main.class); + + private Main() { } + + /** + * Load the Spring Integration Application Context + * + * @param args - command line arguments + */ + public static void main(final String... args) { + + if (LOGGER.isInfoEnabled()) { + LOGGER.info("\n=========================================================" + + "\n " + + "\n Welcome to the Spring Integration Zip Sample " + + "\n " + + "\n For more information please visit: " + + "\n http://www.springsource.org/spring-integration " + + "\n " + + "\n=========================================================" ); + } + + final AbstractApplicationContext context = + new ClassPathXmlApplicationContext("classpath:META-INF/spring/integration/*-context.xml"); + + context.registerShutdownHook(); + + SpringIntegrationUtils.displayDirectories(context); + + final Scanner scanner = new Scanner(System.in); + + if (LOGGER.isInfoEnabled()) { + LOGGER.info("\n=========================================================" + + "\n " + + "\n Please press 'q + Enter' to quit the application. " + + "\n " + + "\n=========================================================" ); + } + + while (!scanner.hasNext("q")) { + //Do nothing unless user presses 'q' to quit. + } + + if (LOGGER.isInfoEnabled()) { + LOGGER.info("Exiting application...bye."); + } + + System.exit(0); + + } +} diff --git a/samples/zip/src/main/java/org/springframework/integration/samples/zip/SpringIntegrationUtils.java b/samples/zip/src/main/java/org/springframework/integration/samples/zip/SpringIntegrationUtils.java new file mode 100644 index 0000000..30b93e4 --- /dev/null +++ b/samples/zip/src/main/java/org/springframework/integration/samples/zip/SpringIntegrationUtils.java @@ -0,0 +1,91 @@ +/* + * Copyright 2015 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 + * + * http://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.samples.zip; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.context.ApplicationContext; +import org.springframework.expression.Expression; +import org.springframework.integration.file.FileReadingMessageSource; +import org.springframework.integration.file.FileWritingMessageHandler; + +/** + * Displays the names of the input and output directories. + * + * @author Gunnar Hillert + * @since 1.0 + * + */ +public final class SpringIntegrationUtils { + + private static final Log logger = LogFactory.getLog(SpringIntegrationUtils.class); + + private SpringIntegrationUtils() { } + + /** + * Helper Method to dynamically determine and display input and output + * directories as defined in the Spring Integration context. + * + * @param context Spring Application Context + */ + public static void displayDirectories(final ApplicationContext context) { + + final Map fileReadingMessageSources = context.getBeansOfType(FileReadingMessageSource.class); + + final List inputDirectories = new ArrayList(); + + for (FileReadingMessageSource source : fileReadingMessageSources.values()) { + final File inDir = (File) new DirectFieldAccessor(source).getPropertyValue("directory"); + inputDirectories.add(inDir.getAbsolutePath()); + } + + + final Map fileWritingMessageHandlers = context.getBeansOfType(FileWritingMessageHandler.class); + + final List outputDirectories = new ArrayList(); + + for (final FileWritingMessageHandler messageHandler : fileWritingMessageHandlers.values()) { + final Expression outDir = (Expression) new DirectFieldAccessor(messageHandler).getPropertyValue("destinationDirectoryExpression"); + outputDirectories.add(outDir.getExpressionString()); + } + + final StringBuilder stringBuilder = new StringBuilder(); + + stringBuilder.append("\n========================================================="); + stringBuilder.append("\n"); + + for (final String inputDirectory : inputDirectories) { + stringBuilder.append("\n Intput directory is: '" + inputDirectory + "'"); + } + + for (final String outputDirectory : outputDirectories) { + stringBuilder.append("\n Output directory is: '" + outputDirectory + "'"); + } + + stringBuilder.append("\n\n========================================================="); + + logger.info(stringBuilder.toString()); + + } + +} diff --git a/samples/zip/src/main/java/org/springframework/integration/samples/zip/TransformationHandler.java b/samples/zip/src/main/java/org/springframework/integration/samples/zip/TransformationHandler.java new file mode 100644 index 0000000..fca42e5 --- /dev/null +++ b/samples/zip/src/main/java/org/springframework/integration/samples/zip/TransformationHandler.java @@ -0,0 +1,70 @@ +/* + * Copyright 2015 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 + * + * http://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.samples.zip; + +import java.io.File; +import java.io.IOException; +import java.util.Locale; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; +import org.springframework.integration.annotation.Transformer; +import org.springframework.integration.file.FileHeaders; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.Message; + +/** + * This Spring Integration transformation handler takes the input file, converts + * the file into a string, converts the file contents into an upper-case string + * and then sets a few Spring Integration message headers. + * + * @author Gunnar Hillert + * @since 1.0 + */ +public class TransformationHandler { + + /** + * Actual Spring Integration transformation handler. + * + * @param inputMessage Spring Integration input message + * @return New Spring Integration message with updated headers + */ + @Transformer + public Message handleFile(final Message inputMessage) { + + final File inputFile = inputMessage.getPayload(); + final String filename = inputFile.getName(); + final String fileExtension = FilenameUtils.getExtension(filename); + + final String inputAsString; + + try { + inputAsString = FileUtils.readFileToString(inputFile); + } catch (IOException e) { + throw new IllegalStateException(e); + } + + final Message message = MessageBuilder.withPayload(inputAsString.toUpperCase(Locale.ENGLISH)) + .setHeader(FileHeaders.FILENAME, filename) + .setHeader(FileHeaders.ORIGINAL_FILE, inputFile) + .setHeader("file_size", inputFile.length()) + .setHeader("file_extension", fileExtension) + .build(); + + return message; + } +} diff --git a/samples/zip/src/main/resources/META-INF/spring/integration/spring-integration-context.xml b/samples/zip/src/main/resources/META-INF/spring/integration/spring-integration-context.xml new file mode 100644 index 0000000..5617c03 --- /dev/null +++ b/samples/zip/src/main/resources/META-INF/spring/integration/spring-integration-context.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/zip/src/main/resources/log4j.xml b/samples/zip/src/main/resources/log4j.xml new file mode 100644 index 0000000..f4d8379 --- /dev/null +++ b/samples/zip/src/main/resources/log4j.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/zip/src/test/java/org/springframework/integration/samples/zip/SpringIntegrationProjectStartupTest.java b/samples/zip/src/test/java/org/springframework/integration/samples/zip/SpringIntegrationProjectStartupTest.java new file mode 100644 index 0000000..a93fa73 --- /dev/null +++ b/samples/zip/src/test/java/org/springframework/integration/samples/zip/SpringIntegrationProjectStartupTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2015 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 + * + * http://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.samples.zip; + +import org.junit.Test; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import org.springframework.integration.samples.zip.SpringIntegrationUtils; + +/** + * Verify that the Spring Integration Application Context starts successfully. + * + * @author Gunnar Hillert + * @since 1.0 + * + */ +public class SpringIntegrationProjectStartupTest { + + @Test + public void testStartupOfSpringInegrationContext() throws Exception{ + final ApplicationContext context + = new ClassPathXmlApplicationContext("/META-INF/spring/integration/spring-integration-context.xml", + SpringIntegrationProjectStartupTest.class); + SpringIntegrationUtils.displayDirectories(context); + Thread.sleep(2000); + } + +} diff --git a/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/support/ConsumerConfigurationTests.java b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/support/ConsumerConfigurationTests.java new file mode 100644 index 0000000..86692fc --- /dev/null +++ b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/support/ConsumerConfigurationTests.java @@ -0,0 +1,610 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.kafka.support; + +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.*; + +import java.util.*; + +import kafka.consumer.ConsumerIterator; +import kafka.consumer.KafkaStream; +import kafka.javaapi.consumer.ConsumerConnector; +import kafka.message.MessageAndMetadata; +import kafka.serializer.Decoder; + +import org.junit.Assert; +import org.junit.Test; + +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +/** + * @author Soby Chacko + * @author Rajasekar Elango + * @since 0.5 + */ +public class ConsumerConfigurationTests { + + @Test + @SuppressWarnings("unchecked") + public void testReceiveMessageForSingleTopicFromSingleStream() { + final ConsumerMetadata consumerMetadata = mock(ConsumerMetadata.class); + final ConsumerConnectionProvider consumerConnectionProvider = + mock(ConsumerConnectionProvider.class); + final MessageLeftOverTracker messageLeftOverTracker = mock(MessageLeftOverTracker.class); + final ConsumerConnector consumerConnector = mock(ConsumerConnector.class); + + Map topicStreamMap = new HashMap(); + topicStreamMap.put("topic1", 1); + when(consumerMetadata.getTopicStreamMap()).thenReturn(topicStreamMap); + + when(consumerConnectionProvider.getConsumerConnector()).thenReturn(consumerConnector); + + final ConsumerConfiguration consumerConfiguration = new ConsumerConfiguration(consumerMetadata, + consumerConnectionProvider, messageLeftOverTracker); + consumerConfiguration.setMaxMessages(1); + + final KafkaStream stream = mock(KafkaStream.class); + final List> streams = new ArrayList>(); + streams.add(stream); + final Map>> messageStreams = new HashMap>>(); + messageStreams.put("topic", streams); + + when(consumerConfiguration.createMessageStreamsForTopic()).thenReturn(messageStreams); + final ConsumerIterator iterator = mock(ConsumerIterator.class); + when(stream.iterator()).thenReturn(iterator); + final MessageAndMetadata messageAndMetadata = mock(MessageAndMetadata.class); + when(iterator.next()).thenReturn(messageAndMetadata); + when(messageAndMetadata.message()).thenReturn((V) "got message"); + when(messageAndMetadata.topic()).thenReturn("topic"); + when(messageAndMetadata.partition()).thenReturn(1); + + final Map>> messages = consumerConfiguration.receive(); + Assert.assertEquals(1, messages.size()); + Assert.assertEquals(1, messages.get("topic").size()); + Assert.assertEquals("got message", messages.get("topic").get(1).get(0)); + + verify(stream, times(1)).iterator(); + verify(iterator, times(1)).next(); + verify(messageAndMetadata, times(1)).message(); + verify(messageAndMetadata, times(1)).topic(); + } + + @Test + @SuppressWarnings("unchecked") + public void testReceiveMessageForSingleTopicFromMultipleStreams() { +<<<<<<< HEAD + final ConsumerMetadata consumerMetadata = mock(ConsumerMetadata.class); +======= + final ConsumerMetadata consumerMetadata = Mockito.mock(ConsumerMetadata.class); +>>>>>>> INTEXT-40 - Add ZIP Transformer + final ConsumerConnectionProvider consumerConnectionProvider = + mock(ConsumerConnectionProvider.class); + final MessageLeftOverTracker messageLeftOverTracker = mock(MessageLeftOverTracker.class); + + Map topicStreamMap = new HashMap(); + topicStreamMap.put("topic1", 1); + when(consumerMetadata.getTopicStreamMap()).thenReturn(topicStreamMap); + + final ConsumerConnector consumerConnector = mock(ConsumerConnector.class); + + when(consumerConnectionProvider.getConsumerConnector()).thenReturn(consumerConnector); + + final ConsumerConfiguration consumerConfiguration = new ConsumerConfiguration(consumerMetadata, + consumerConnectionProvider, messageLeftOverTracker); + consumerConfiguration.setMaxMessages(3); + + final KafkaStream stream1 = mock(KafkaStream.class); + final KafkaStream stream2 = mock(KafkaStream.class); + final KafkaStream stream3 = mock(KafkaStream.class); + final List> streams = new ArrayList>(); + streams.add(stream1); + streams.add(stream2); + streams.add(stream3); + final Map>> messageStreams = new HashMap>>(); + messageStreams.put("topic", streams); + + when(consumerConfiguration.createMessageStreamsForTopic()).thenReturn(messageStreams); + final ConsumerIterator iterator1 = mock(ConsumerIterator.class); + final ConsumerIterator iterator2 = mock(ConsumerIterator.class); + final ConsumerIterator iterator3 = mock(ConsumerIterator.class); + + when(stream1.iterator()).thenReturn(iterator1); + when(stream2.iterator()).thenReturn(iterator2); + when(stream3.iterator()).thenReturn(iterator3); + final MessageAndMetadata messageAndMetadata1 = mock(MessageAndMetadata.class); + final MessageAndMetadata messageAndMetadata2 = mock(MessageAndMetadata.class); + final MessageAndMetadata messageAndMetadata3 = mock(MessageAndMetadata.class); + + when(iterator1.next()).thenReturn(messageAndMetadata1); + when(iterator2.next()).thenReturn(messageAndMetadata2); + when(iterator3.next()).thenReturn(messageAndMetadata3); + + when(messageAndMetadata1.message()).thenReturn((V)"got message"); + when(messageAndMetadata1.topic()).thenReturn("topic"); + when(messageAndMetadata1.partition()).thenReturn(1); + + when(messageAndMetadata2.message()).thenReturn((V)"got message"); + when(messageAndMetadata2.topic()).thenReturn("topic"); + when(messageAndMetadata2.partition()).thenReturn(2); + + when(messageAndMetadata3.message()).thenReturn((V)"got message"); + when(messageAndMetadata3.topic()).thenReturn("topic"); + when(messageAndMetadata3.partition()).thenReturn(3); + + final Map>> messages = consumerConfiguration.receive(); + Assert.assertEquals(messages.size(), 1); + int sum = 0; + + final Map> values = messages.get("topic"); + + for (final List l : values.values()) { + sum += l.size(); + } + + Assert.assertEquals(3, sum); + } + + @Test + @SuppressWarnings("unchecked") + public void testReceiveMessageForMultipleTopicsFromMultipleStreams() { +<<<<<<< HEAD + final ConsumerMetadata consumerMetadata = mock(ConsumerMetadata.class); +======= + final ConsumerMetadata consumerMetadata = Mockito.mock(ConsumerMetadata.class); +>>>>>>> INTEXT-40 - Add ZIP Transformer + final ConsumerConnectionProvider consumerConnectionProvider = + mock(ConsumerConnectionProvider.class); + final MessageLeftOverTracker messageLeftOverTracker = mock(MessageLeftOverTracker.class); + + Map topicStreamMap = new HashMap(); + topicStreamMap.put("topic1", 1); + when(consumerMetadata.getTopicStreamMap()).thenReturn(topicStreamMap); + + + final ConsumerConnector consumerConnector = mock(ConsumerConnector.class); + + when(consumerConnectionProvider.getConsumerConnector()).thenReturn(consumerConnector); + + final ConsumerConfiguration consumerConfiguration = new ConsumerConfiguration(consumerMetadata, + consumerConnectionProvider, messageLeftOverTracker); + consumerConfiguration.setMaxMessages(9); + + final KafkaStream stream1 = mock(KafkaStream.class); + final KafkaStream stream2 = mock(KafkaStream.class); + final KafkaStream stream3 = mock(KafkaStream.class); + final List> streams = new ArrayList>(); + streams.add(stream1); + streams.add(stream2); + streams.add(stream3); + final Map>> messageStreams = new HashMap>>(); + messageStreams.put("topic1", streams); + messageStreams.put("topic2", streams); + messageStreams.put("topic3", streams); + + when(consumerConfiguration.createMessageStreamsForTopic()).thenReturn(messageStreams); + final ConsumerIterator iterator1 = mock(ConsumerIterator.class); + final ConsumerIterator iterator2 = mock(ConsumerIterator.class); + final ConsumerIterator iterator3 = mock(ConsumerIterator.class); + + when(stream1.iterator()).thenReturn(iterator1); + when(stream2.iterator()).thenReturn(iterator2); + when(stream3.iterator()).thenReturn(iterator3); + final MessageAndMetadata messageAndMetadata1 = mock(MessageAndMetadata.class); + final MessageAndMetadata messageAndMetadata2 = mock(MessageAndMetadata.class); + final MessageAndMetadata messageAndMetadata3 = mock(MessageAndMetadata.class); + + when(iterator1.next()).thenReturn(messageAndMetadata1); + when(iterator2.next()).thenReturn(messageAndMetadata2); + when(iterator3.next()).thenReturn(messageAndMetadata3); + + when(messageAndMetadata1.message()).thenReturn((V)"got message1"); + when(messageAndMetadata1.topic()).thenReturn("topic1"); + when(messageAndMetadata1.partition()).thenAnswer(getAnswer()); + + when(messageAndMetadata2.message()).thenReturn((V)"got message2"); + when(messageAndMetadata2.topic()).thenReturn("topic2"); + when(messageAndMetadata1.partition()).thenAnswer(getAnswer()); + + when(messageAndMetadata3.message()).thenReturn((V)"got message3"); + when(messageAndMetadata3.topic()).thenReturn("topic3"); + when(messageAndMetadata1.partition()).thenAnswer(getAnswer()); + + final Map>> messages = consumerConfiguration.receive(); + + int sum = 0; + + final Collection>> values = messages.values(); + + for (final Map> m : values) { + for (final List l : m.values()) { + sum += l.size(); + } + } + + Assert.assertEquals(9, sum); + } + + + + private Answer getAnswer() { + return new Answer() { + private int count = 0; + + @Override + public Object answer(final InvocationOnMock invocation) throws Throwable { + if (count++ == 1) { + return 1; + } else if (count++ == 2) { + return 2; + } + + return 3; + } + }; + } + + @Test + @SuppressWarnings("unchecked") + public void testReceiveMessageAndVerifyMessageLeftoverFromPreviousPollAreTakenFirst() { +<<<<<<< HEAD + final ConsumerMetadata consumerMetadata = mock(ConsumerMetadata.class); +======= + final ConsumerMetadata consumerMetadata = Mockito.mock(ConsumerMetadata.class); +>>>>>>> INTEXT-40 - Add ZIP Transformer + final ConsumerConnectionProvider consumerConnectionProvider = + mock(ConsumerConnectionProvider.class); + final MessageLeftOverTracker messageLeftOverTracker = mock(MessageLeftOverTracker.class); + final ConsumerConnector consumerConnector = mock(ConsumerConnector.class); + + Map topicStreamMap = new HashMap(); + topicStreamMap.put("topic1", 1); + when(consumerMetadata.getTopicStreamMap()).thenReturn(topicStreamMap); + when(messageLeftOverTracker.getCurrentCount()).thenReturn(3); + + final MessageAndMetadata m1 = mock(MessageAndMetadata.class); + final MessageAndMetadata m2 = mock(MessageAndMetadata.class); + final MessageAndMetadata m3 = mock(MessageAndMetadata.class); + + when(m1.key()).thenReturn("key1"); + when(m1.message()).thenReturn("value1"); + when(m1.topic()).thenReturn("topic1"); + when(m1.partition()).thenReturn(1); + + when(m2.key()).thenReturn("key2"); + when(m2.message()).thenReturn("value2"); + when(m2.topic()).thenReturn("topic2"); + when(m2.partition()).thenReturn(1); + + when(m3.key()).thenReturn("key1"); + when(m3.message()).thenReturn("value3"); + when(m3.topic()).thenReturn("topic3"); + when(m3.partition()).thenReturn(1); + + final List> mList = new ArrayList>(); + mList.add(m1); + mList.add(m2); + mList.add(m3); + + when((List>) (Object) messageLeftOverTracker.getMessageLeftOverFromPreviousPoll()).thenReturn(mList); + + when(consumerConnectionProvider.getConsumerConnector()).thenReturn(consumerConnector); + + final ConsumerConfiguration consumerConfiguration = new ConsumerConfiguration(consumerMetadata, + consumerConnectionProvider, messageLeftOverTracker); + consumerConfiguration.setMaxMessages(5); + + final KafkaStream stream = mock(KafkaStream.class); + final List> streams = new ArrayList>(); + streams.add(stream); + final Map>> messageStreams = new HashMap>>(); + messageStreams.put("topic1", streams); + when(consumerConfiguration.createMessageStreamsForTopic()).thenReturn(messageStreams); + final ConsumerIterator iterator = mock(ConsumerIterator.class); + when(stream.iterator()).thenReturn(iterator); + final MessageAndMetadata messageAndMetadata = mock(MessageAndMetadata.class); + when(iterator.next()).thenReturn(messageAndMetadata); + when(messageAndMetadata.message()).thenReturn((V) "got message"); + when(messageAndMetadata.topic()).thenReturn("topic1"); + when(messageAndMetadata.partition()).thenReturn(1); + + final Map>> messages = consumerConfiguration.receive(); + int sum = 0; + + final Collection>> values = messages.values(); + + for (final Map> m : values) { + for (final List l : m.values()) { + sum += l.size(); + } + + } + Assert.assertEquals(5, sum); + + Assert.assertTrue(messages.containsKey("topic1")); + Assert.assertTrue(messages.containsKey("topic2")); + Assert.assertTrue(messages.containsKey("topic3")); + + Assert.assertTrue(valueFound(messages.get("topic1").get(1), "value1")); + Assert.assertTrue(valueFound(messages.get("topic2").get(1), "value2")); + Assert.assertTrue(valueFound(messages.get("topic3").get(1), "value3")); + } + + @Test +<<<<<<< HEAD + @SuppressWarnings("unchecked") + public void testGetConsumerMapWithMessageStreamsWithNullDecoders() { + + final ConsumerMetadata mockedConsumerMetadata = mock(ConsumerMetadata.class); + + assertNull(mockedConsumerMetadata.getKeyDecoder()); + assertNull(mockedConsumerMetadata.getValueDecoder()); + + final Map topicsStreamMap = new HashMap(); + when(mockedConsumerMetadata.getTopicStreamMap()).thenReturn(topicsStreamMap); + + final ConsumerConnectionProvider mockedConsumerConnectionProvider = mock(ConsumerConnectionProvider.class); + final MessageLeftOverTracker mockedMessageLeftOverTracker = mock(MessageLeftOverTracker.class); + final ConsumerConnector mockedConsumerConnector = mock(ConsumerConnector.class); + + when(mockedConsumerConnectionProvider.getConsumerConnector()).thenReturn(mockedConsumerConnector); + + final Map>> messageStreams = new HashMap>>(); + when((Map>>) + (Object) mockedConsumerConnector.createMessageStreams(topicsStreamMap)).thenReturn(messageStreams); + + final ConsumerConfiguration consumerConfiguration = new ConsumerConfiguration(mockedConsumerMetadata, + mockedConsumerConnectionProvider, mockedMessageLeftOverTracker); + + consumerConfiguration.createMessageStreamsForTopic(); + + verify(mockedConsumerMetadata, atLeast(1)).getTopicStreamMap(); + verify(mockedConsumerConnector, atLeast(1)).createMessageStreams(topicsStreamMap, null, null); + //verify(mockedConsumerConnector, atMost(0)).createMessageStreams(topicsStreamMap, null, null); + } + + @Test + @SuppressWarnings("unchecked") + public void testGetConsumerMapWithMessageStreamsWithDecoders() { + + @SuppressWarnings("unchecked") + final ConsumerMetadata mockedConsumerMetadata = mock(ConsumerMetadata.class); + + final Map topicsStreamMap = new HashMap(); + when(mockedConsumerMetadata.getTopicStreamMap()).thenReturn(topicsStreamMap); + + @SuppressWarnings("unchecked") + final Decoder mockedKeyDecoder = mock(Decoder.class); + + @SuppressWarnings("unchecked") + final Decoder mockedValueDecoder = mock(Decoder.class); + + when(mockedConsumerMetadata.getKeyDecoder()).thenReturn(mockedKeyDecoder); + when(mockedConsumerMetadata.getValueDecoder()).thenReturn(mockedValueDecoder); + + final ConsumerConnectionProvider mockedConsumerConnectionProvider = + mock(ConsumerConnectionProvider.class); + final MessageLeftOverTracker mockedMessageLeftOverTracker = mock(MessageLeftOverTracker.class); + final ConsumerConnector mockedConsumerConnector = mock(ConsumerConnector.class); + + when(mockedConsumerConnectionProvider.getConsumerConnector()).thenReturn(mockedConsumerConnector); + + final Map>> messageStreams = new HashMap>>(); + when(mockedConsumerConnector.createMessageStreams(topicsStreamMap)).thenReturn(messageStreams); + + final ConsumerConfiguration consumerConfiguration = + new ConsumerConfiguration(mockedConsumerMetadata, mockedConsumerConnectionProvider, + mockedMessageLeftOverTracker); + + consumerConfiguration.createMessageStreamsForTopic(); + + verify(mockedConsumerMetadata, atLeast(1)).getTopicStreamMap(); + verify(mockedConsumerConnector, atMost(0)).createMessageStreams(topicsStreamMap); + verify(mockedConsumerConnector, atLeast(1)) + .createMessageStreams(topicsStreamMap, mockedKeyDecoder, mockedValueDecoder); + } + + + @Test + @SuppressWarnings("unchecked") + public void testReceiveMessageForTopicFilterFromSingleStream() { + final ConsumerMetadata consumerMetadata = mock(ConsumerMetadata.class); + final ConsumerConnectionProvider consumerConnectionProvider = + mock(ConsumerConnectionProvider.class); + final MessageLeftOverTracker messageLeftOverTracker = mock(MessageLeftOverTracker.class); + final ConsumerConnector consumerConnector = mock(ConsumerConnector.class); + + when(consumerMetadata.getTopicFilterConfiguration()).thenReturn(new TopicFilterConfiguration(".*", 1, false)); + + when(consumerConnectionProvider.getConsumerConnector()).thenReturn(consumerConnector); + + final ConsumerConfiguration consumerConfiguration = + new ConsumerConfiguration(consumerMetadata, consumerConnectionProvider, + messageLeftOverTracker); + consumerConfiguration.setMaxMessages(1); + + final KafkaStream stream = mock(KafkaStream.class); + final List> streams = new ArrayList>(); + streams.add(stream); + + when(consumerConfiguration.createMessageStreamsForTopicFilter()).thenReturn(streams); + final ConsumerIterator iterator = mock(ConsumerIterator.class); + when(stream.iterator()).thenReturn(iterator); + final MessageAndMetadata messageAndMetadata = mock(MessageAndMetadata.class); + when(iterator.next()).thenReturn(messageAndMetadata); + when(messageAndMetadata.message()).thenReturn("got message"); + when(messageAndMetadata.topic()).thenReturn("topic"); + when(messageAndMetadata.partition()).thenReturn(1); + + final Map>> messages = consumerConfiguration.receive(); + Assert.assertEquals(1, messages.size()); + Assert.assertEquals(1, messages.get("topic").size()); + Assert.assertEquals("got message", messages.get("topic").get(1).get(0)); + + verify(stream, times(1)).iterator(); + verify(iterator, times(1)).next(); + verify(messageAndMetadata, times(1)).message(); + verify(messageAndMetadata, times(1)).topic(); + } + + @Test + @SuppressWarnings("unchecked") + public void testReceiveMessageForTopicFilterFromMultipleStreams() { + final ConsumerMetadata consumerMetadata = mock(ConsumerMetadata.class); + final ConsumerConnectionProvider consumerConnectionProvider = + mock(ConsumerConnectionProvider.class); + final MessageLeftOverTracker messageLeftOverTracker = mock(MessageLeftOverTracker.class); + + when(consumerMetadata.getTopicFilterConfiguration()).thenReturn(new TopicFilterConfiguration(".*", 1, false)); + + final ConsumerConnector consumerConnector = mock(ConsumerConnector.class); + + when(consumerConnectionProvider.getConsumerConnector()).thenReturn(consumerConnector); + + final ConsumerConfiguration consumerConfiguration = + new ConsumerConfiguration(consumerMetadata, consumerConnectionProvider, + messageLeftOverTracker); + consumerConfiguration.setMaxMessages(3); + + final KafkaStream stream1 = mock(KafkaStream.class); + final KafkaStream stream2 = mock(KafkaStream.class); + final KafkaStream stream3 = mock(KafkaStream.class); + final List> streams = new ArrayList>(); + streams.add(stream1); + streams.add(stream2); + streams.add(stream3); + + when(consumerConfiguration.createMessageStreamsForTopicFilter()).thenReturn(streams); + final ConsumerIterator iterator1 = mock(ConsumerIterator.class); + final ConsumerIterator iterator2 = mock(ConsumerIterator.class); + final ConsumerIterator iterator3 = mock(ConsumerIterator.class); + + when(stream1.iterator()).thenReturn(iterator1); + when(stream2.iterator()).thenReturn(iterator2); + when(stream3.iterator()).thenReturn(iterator3); + final MessageAndMetadata messageAndMetadata1 = mock(MessageAndMetadata.class); + final MessageAndMetadata messageAndMetadata2 = mock(MessageAndMetadata.class); + final MessageAndMetadata messageAndMetadata3 = mock(MessageAndMetadata.class); + + when(iterator1.next()).thenReturn(messageAndMetadata1); + when(iterator2.next()).thenReturn(messageAndMetadata2); + when(iterator3.next()).thenReturn(messageAndMetadata3); + + when(messageAndMetadata1.message()).thenReturn("got message".getBytes()); + when(messageAndMetadata1.topic()).thenReturn("topic"); + when(messageAndMetadata1.partition()).thenReturn(1); + + when(messageAndMetadata2.message()).thenReturn("got message".getBytes()); + when(messageAndMetadata2.topic()).thenReturn("topic"); + when(messageAndMetadata2.partition()).thenReturn(2); + + when(messageAndMetadata3.message()).thenReturn("got message".getBytes()); + when(messageAndMetadata3.topic()).thenReturn("topic"); + when(messageAndMetadata3.partition()).thenReturn(3); + + final Map>> messages = consumerConfiguration.receive(); + Assert.assertEquals(1, messages.size()); + int sum = 0; + + final Map> values = messages.get("topic"); + + for (final List l : values.values()) { + sum += l.size(); + } + + Assert.assertEquals(3, sum); +======= + public void testGetConsumerMapWithMessageStreamsWithNullDecoders() { + + final ConsumerMetadata consumerMetadata = Mockito.mock(ConsumerMetadata.class); + + final Map topicsStreamMap = new HashMap(); + when(consumerMetadata.getTopicStreamMap()).thenReturn(topicsStreamMap); + + final ConsumerConnectionProvider consumerConnectionProvider = + Mockito.mock(ConsumerConnectionProvider.class); + final MessageLeftOverTracker messageLeftOverTracker = Mockito.mock(MessageLeftOverTracker.class); + final ConsumerConnector consumerConnector = Mockito.mock(ConsumerConnector.class); + + Mockito.when(consumerConnectionProvider.getConsumerConnector()).thenReturn(consumerConnector); + + final Map>> messageStreams = new HashMap>>(); + Mockito.when(consumerConnector.createMessageStreams(topicsStreamMap)).thenReturn(messageStreams); + Mockito.when(consumerConnector.createMessageStreams(topicsStreamMap)).thenReturn(messageStreams); + + final ConsumerConfiguration consumerConfiguration = new ConsumerConfiguration(consumerMetadata, + consumerConnectionProvider, messageLeftOverTracker); + + consumerConfiguration.getConsumerMapWithMessageStreams(); + + verify(consumerMetadata, atLeast(1)).getTopicStreamMap(); + verify(consumerConnector, atLeast(1)).createMessageStreams(topicsStreamMap); + verify(consumerConnector, atMost(0)).createMessageStreams(topicsStreamMap, null, null); + + } + + @Test + public void testGetConsumerMapWithMessageStreamsWithDecoders() { + + @SuppressWarnings("unchecked") + final ConsumerMetadata consumerMetadata = mock(ConsumerMetadata.class); + + final Map topicsStreamMap = new HashMap(); + when(consumerMetadata.getTopicStreamMap()).thenReturn(topicsStreamMap); + + @SuppressWarnings("unchecked") + final Decoder mockedKeyDecoder = (Decoder) mock(Decoder.class); + + @SuppressWarnings("unchecked") + final Decoder mockedValueDecoder = (Decoder) mock(Decoder.class); + + when(consumerMetadata.getKeyDecoder()).thenReturn(mockedKeyDecoder); + when(consumerMetadata.getValueDecoder()).thenReturn(mockedValueDecoder); + + final ConsumerConnectionProvider consumerConnectionProvider = + Mockito.mock(ConsumerConnectionProvider.class); + final MessageLeftOverTracker messageLeftOverTracker = Mockito.mock(MessageLeftOverTracker.class); + final ConsumerConnector consumerConnector = Mockito.mock(ConsumerConnector.class); + + Mockito.when(consumerConnectionProvider.getConsumerConnector()).thenReturn(consumerConnector); + + final Map>> messageStreams = new HashMap>>(); + Mockito.when(consumerConnector.createMessageStreams(topicsStreamMap)).thenReturn(messageStreams); + Mockito.when(consumerConnector.createMessageStreams(topicsStreamMap)).thenReturn(messageStreams); + + final ConsumerConfiguration consumerConfiguration = new ConsumerConfiguration(consumerMetadata, + consumerConnectionProvider, messageLeftOverTracker); + + consumerConfiguration.getConsumerMapWithMessageStreams(); + + verify(consumerMetadata, atLeast(1)).getTopicStreamMap(); + verify(consumerConnector, atMost(0)).createMessageStreams(topicsStreamMap); + verify(consumerConnector, atLeast(1)).createMessageStreams(topicsStreamMap, mockedKeyDecoder, mockedValueDecoder); + +>>>>>>> INTEXT-40 - Add ZIP Transformer + } + + private boolean valueFound(final List l, final String value){ + for (final Object o : l){ + if (value.equals(o)){ + return true; + } + } + + return false; + } +} diff --git a/spring-integration-zip/README.md b/spring-integration-zip/README.md new file mode 100644 index 0000000..10fc814 --- /dev/null +++ b/spring-integration-zip/README.md @@ -0,0 +1,171 @@ +Spring Integration Zip Support +============================== + +| | Build Status | +| ------------- | :-------------: | +| Linux | [![Build Status](http://build.spring.io/plugins/servlet/buildStatusImage/INTEXT-ZIP)](https://build.spring.io/browse/INTEXT-ZIP) | +| Windows | [![Build Status](http://build.spring.io/plugins/servlet/buildStatusImage/INTEXT-ZIPWIN)](https://build.spring.io/browse/INTEXT-ZIPWIN) | + +## Introduction + +This *Spring Integration Extension* provides [Zip][] (un-) compression support. The following components are provided: + +* Zip transformer +* Unzip transformer +* UnZipResultSplitter + +**Important!** This module is currently under active development and not all functionality is provided or stable, yet. + +## ZIP Compression Support + +The following input data types can be **compressed**: + +* File +* String +* byte[] +* Iterable + +In input data types can be mixed as part of an Iterable. E.g. you should be +easily be able 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 zip transformer 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. + +## ZIP Un-compression Support + +The following input data types can be **decompressed**: + +* File +* InputStream +* byte[] + +When unzipping data, you can also specify a property **expectSingleResult**. 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* (the *default*), +then the payload will be of type *SortedMap*, if *true*, however, the actual zip +entry will be returned. + +Othe 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*. + +### workDirectory + +Set the work-directory. The work directory is used when the ZipResultType is set to ZipResultType.FILE. By default this property is set to the System temporary directory containing a sub-directory "ziptransformer". + +### ZipResultType + +Defines the format of the data returned after transformation. Available options are: + +* File +* byte[] + +## UnZipResultSplitter + +The `UnZipResultSplitter` is useful in cases where Zip files contain more than *1* +zip entry. + +## Zipping and Unzipping Large Files + +TBD + +## Java Package Structure + +### Base package + +The base package `org.springframework.integration.zip` contains the *ZipHeaders* class which defines the *Spring Integration* message headers that are specific to the Zip module. + +### config.xml + +This package contains the parser classes for the XML Namespace support. + +### transformer + +Contain the classes responsible for the actual (un-) zip operation: + +* ZipTransformer +* UnZipTransformer + +## Namespace Support + +Full XML namespace support is provided. + +## Building the Project + +To build and install jars into your local Maven cache, please execute: + + ./gradlew install + +If you encounter out of memory errors during the build, increase available heap and permgen for Gradle: + + GRADLE_OPTS='-XX:MaxPermSize=1024m -Xmx1024m' + +To build api Javadoc (results will be in `build/api`): + + ./gradlew api + +To build complete distribution including `-dist` and `-schema` zip files (results will be in `build/distributions`) + + ./gradlew dist + +# IDE Support + +While your custom Spring Integration Adapter is initially created with SpringSource Tool Suite, you in fact end up with a Gradle-based project. As such, the created project can be imported into other IDEs as well. + +## Using Spring Tool Suite + +Gradle projects can be directly imported into STS. But please make sure that you have the Gradle support installed. + +## Using Plain Eclipse + +To generate Eclipse metadata (*.classpath* and *.project* files), do the following: + + ./gradlew eclipse + +Once complete, you may then import the project into Eclipse as usual: + + *File -> Import -> Existing projects into workspace* + +Browse to the root directory of the project and it should import free of errors. + +## Using IntelliJ IDEA + +To generate IDEA metadata (.iml and .ipr files), do the following: + + ./gradlew idea + +# Further Resources + +## Getting support + +Check out the [spring-integration][spring-integration tag] tag on [Stack Overflow][]. + +## Related GitHub projects + +* [Spring Integration][] +* [Spring Integration Samples][] +* [Spring Integration Templates][] +* [Spring Integration Dsl Groovy][] +* [Spring Integration Dsl Scala][] + +For more information, please also don't forget to visit the [Spring Integration][] website. + +[Spring Integration]: https://github.com/SpringSource/spring-integration +[spring-integration tag]: http://stackoverflow.com/questions/tagged/spring-integration +[Spring Integration Samples]: https://github.com/SpringSource/spring-integration-samples +[Spring Integration Templates]: https://github.com/SpringSource/spring-integration-templates/tree/master/si-sts-templates +[Spring Integration Dsl Groovy]: https://github.com/SpringSource/spring-integration-dsl-groovy +[Spring Integration Dsl Scala]: https://github.com/SpringSource/spring-integration-dsl-scala +[Stack Overflow]: http://stackoverflow.com/faq + +[Zip]: http://en.wikipedia.org/wiki/Zip_%28file_format%29 \ No newline at end of file diff --git a/spring-integration-zip/build.gradle b/spring-integration-zip/build.gradle new file mode 100644 index 0000000..4bbfbc0 --- /dev/null +++ b/spring-integration-zip/build.gradle @@ -0,0 +1,243 @@ +description = 'Spring Integration Zip Adapter' + +buildscript { + repositories { + maven { url 'https://repo.springsource.org/plugins-snapshot' } + } +} + +apply plugin: 'java' +apply from: "${rootProject.projectDir}/publish-maven.gradle" +apply plugin: 'eclipse' +apply plugin: 'idea' + +group = 'org.springframework.integration' + +repositories { + maven { url 'http://repo.springsource.org/libs-milestone' } + maven { url 'http://repo.springsource.org/plugins-release' } +} + +sourceCompatibility=1.6 +targetCompatibility=1.6 + +ext { + + linkHomepage = 'https://github.com/spring-projects/spring-integration-extensions' + linkCi = 'https://build.spring.io/browse/INTEXT-ZIP' + linkIssue = 'https://jira.spring.io/browse/INTEXT' + linkScmUrl = 'https://github.com/spring-projects/spring-integration-extensions' + linkScmConnection = 'https://github.com/spring-projects/spring-integration-extensions.git' + linkScmDevConnection = 'git@github.com:spring-projects/spring-integration-extensions.git' + + junitVersion = '4.11' + log4jVersion = '1.2.17' + mockitoVersion = '1.9.5' + springVersion = '4.0.7.RELEASE' + springIntegrationVersion = '4.2.0.M1' + ztZipVersion = '1.8' + + idPrefix = 'zip' +} + +eclipse { + project { + natures += 'org.springframework.ide.eclipse.core.springnature' + } +} + +sourceSets { + test { + resources { + srcDirs = ['src/test/resources', 'src/test/java'] + } + } +} + +// See http://www.gradle.org/docs/current/userguide/dependency_management.html#sub:configurations +// and http://www.gradle.org/docs/current/dsl/org.gradle.api.artifacts.ConfigurationContainer.html +configurations { + jacoco //Configuration Group used by Sonar to provide Code Coverage using JaCoCo +} + +dependencies { + compile "org.springframework.integration:spring-integration-core:$springIntegrationVersion" + compile "org.springframework.integration:spring-integration-file:$springIntegrationVersion" + compile "org.zeroturnaround:zt-zip:$ztZipVersion" + testCompile "org.springframework.integration:spring-integration-test:$springIntegrationVersion" + testCompile "junit:junit:$junitVersion" + testCompile "log4j:log4j:$log4jVersion" + testCompile "org.mockito:mockito-all:$mockitoVersion" + testCompile "org.springframework:spring-test:$springVersion" + jacoco group: "org.jacoco", name: "org.jacoco.agent", version: "0.7.4.201502262128", classifier: "runtime" +} + + +// enable all compiler warnings; individual projects may customize further +ext.xLintArg = '-Xlint:all' +[compileJava, compileTestJava]*.options*.compilerArgs = [xLintArg] + +test { + // suppress all console output during testing unless running `gradle -i` + logging.captureStandardOutput(LogLevel.INFO) + jvmArgs "-javaagent:${configurations.jacoco.asPath}=destfile=${buildDir}/jacoco.exec,includes=*" +} + +task sourcesJar(type: Jar) { + classifier = 'sources' + from sourceSets.main.allJava +} + +task javadocJar(type: Jar) { + classifier = 'javadoc' + from javadoc +} + +artifacts { + archives sourcesJar + archives javadocJar +} + +apply plugin: 'sonar-runner' + +sonarRunner { + sonarProperties { + property "sonar.jacoco.reportPath", "${buildDir.name}/jacoco.exec" + property "sonar.links.homepage", linkHomepage + property "sonar.links.ci", linkCi + property "sonar.links.issue", linkIssue + property "sonar.links.scm", linkScmUrl + property "sonar.links.scm_dev", linkScmDevConnection + property "sonar.java.coveragePlugin", "jacoco" + } +} + +task api(type: Javadoc) { + group = 'Documentation' + description = 'Generates the Javadoc API documentation.' + title = "${rootProject.description} ${version} API" + options.memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED + options.author = true + options.header = rootProject.description + options.overview = 'src/api/overview.html' + + source = sourceSets.main.allJava + classpath = project.sourceSets.main.compileClasspath + destinationDir = new File(buildDir, "api") +} + +task schemaZip(type: Zip) { + group = 'Distribution' + classifier = 'schema' + description = "Builds -${classifier} archive containing all " + + "XSDs for deployment at static.springframework.org/schema." + + def Properties schemas = new Properties(); + def shortName = idPrefix.replaceFirst("${idPrefix}-", '') + + project.sourceSets.main.resources.find { + it.path.endsWith('META-INF/spring.schemas') + }?.withInputStream { schemas.load(it) } + + for (def key : schemas.keySet()) { + File xsdFile = project.sourceSets.main.resources.find { + it.path.endsWith(schemas.get(key)) + } + assert xsdFile != null + into ("integration/${shortName}") { + from xsdFile.path + } + } + +} + +task docsZip(type: Zip) { + group = 'Distribution' + classifier = 'docs' + description = "Builds -${classifier} archive containing api docs " + + "for deployment at static.springframework.org/spring-integration/docs." + + from('src/dist') { + include 'changelog.txt' + } + + from (api) { + into 'api' + } + +} + +task distZip(type: Zip, dependsOn: [docsZip, schemaZip]) { + group = 'Distribution' + classifier = 'dist' + description = "Builds -${classifier} archive, containing all jars and docs, " + + "suitable for community download page." + + ext.baseDir = "${project.name}-${project.version}"; + + from('src/dist') { + include 'readme.txt' + include 'license.txt' + include 'notice.txt' + into "${baseDir}" + } + + from(zipTree(docsZip.archivePath)) { + into "${baseDir}/docs" + } + + from(zipTree(schemaZip.archivePath)) { + into "${baseDir}/schema" + } + + into ("${baseDir}/libs") { + from project.jar + from project.sourcesJar + from project.javadocJar + } +} + +// Create an optional "with dependencies" distribution. +// Not published by default; only for use when building from source. +task depsZip(type: Zip, dependsOn: distZip) { zipTask -> + group = 'Distribution' + classifier = 'dist-with-deps' + description = "Builds -${classifier} archive, containing everything " + + "in the -${distZip.classifier} archive plus all dependencies." + + from zipTree(distZip.archivePath) + + gradle.taskGraph.whenReady { taskGraph -> + if (taskGraph.hasTask(":${zipTask.name}")) { + def projectName = rootProject.name + def artifacts = new HashSet() + + rootProject.configurations.runtime.resolvedConfiguration.resolvedArtifacts.each { artifact -> + def dependency = artifact.moduleVersion.id + if (!projectName.equals(dependency.name)) { + artifacts << artifact.file + } + } + + zipTask.from(artifacts) { + into "${distZip.baseDir}/deps" + } + } + } +} + +artifacts { + archives distZip + archives docsZip + archives schemaZip +} + +task dist(dependsOn: assemble) { + group = 'Distribution' + description = 'Builds -dist, -docs and -schema distribution archives.' +} + +task wrapper(type: Wrapper) { + description = 'Generates gradlew[.bat] scripts' + gradleVersion = '2.4' +} diff --git a/spring-integration-zip/gradle.properties b/spring-integration-zip/gradle.properties new file mode 100644 index 0000000..bebfcbc --- /dev/null +++ b/spring-integration-zip/gradle.properties @@ -0,0 +1 @@ +version=1.0.0.BUILD-SNAPSHOT diff --git a/spring-integration-zip/gradle/wrapper/gradle-wrapper.jar b/spring-integration-zip/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..3d0dee6e8edfecc92e04653ec780de06f7b34f8b GIT binary patch literal 51017 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 H-~Rf4?cF@p literal 0 HcmV?d00001 diff --git a/spring-integration-zip/gradle/wrapper/gradle-wrapper.properties b/spring-integration-zip/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..1330889 --- /dev/null +++ b/spring-integration-zip/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue May 12 22:21:08 EDT 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.3-bin.zip diff --git a/spring-integration-zip/gradlew b/spring-integration-zip/gradlew new file mode 100755 index 0000000..91a7e26 --- /dev/null +++ b/spring-integration-zip/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/spring-integration-zip/gradlew.bat b/spring-integration-zip/gradlew.bat new file mode 100644 index 0000000..aec9973 --- /dev/null +++ b/spring-integration-zip/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/spring-integration-zip/publish-maven.gradle b/spring-integration-zip/publish-maven.gradle new file mode 100644 index 0000000..6294f70 --- /dev/null +++ b/spring-integration-zip/publish-maven.gradle @@ -0,0 +1,61 @@ +apply plugin: 'maven' + +ext.optionalDeps = [] +ext.providedDeps = [] + +ext.optional = { optionalDeps << it } +ext.provided = { providedDeps << it } + +install { + repositories.mavenInstaller { + customizePom(pom, project) + } +} + +def customizePom(pom, gradleProject) { + pom.whenConfigured { generatedPom -> + // respect 'optional' and 'provided' dependencies + gradleProject.optionalDeps.each { dep -> + generatedPom.dependencies.find { it.artifactId == dep.name }?.optional = true + } + gradleProject.providedDeps.each { dep -> + generatedPom.dependencies.find { it.artifactId == dep.name }?.scope = 'provided' + } + + // eliminate test-scoped dependencies (no need in maven central poms) + generatedPom.dependencies.removeAll { dep -> + dep.scope == 'test' + } + + // add all items necessary for maven central publication + generatedPom.project { + name = gradleProject.description + description = gradleProject.description + url = 'https://github.com/SpringSource/spring-integration-extensions' + organization { + name = 'SpringSource' + url = 'http://springsource.org' + } + licenses { + license { + name 'The Apache Software License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution 'repo' + } + } + scm { + url = 'https://github.com/SpringSource/spring-integration-extensions' + connection = 'scm:git:git://github.com/SpringSource/spring-integration-extensions' + developerConnection = 'scm:git:git://github.com/SpringSource/spring-integration-extensions' + } + + developers { + developer { + id = 'not specified' + name = 'Gunnar Hillert' + email = 'not specified' + } + } + } + } +} diff --git a/spring-integration-zip/src/api/overview.html b/spring-integration-zip/src/api/overview.html new file mode 100644 index 0000000..fb0198b --- /dev/null +++ b/spring-integration-zip/src/api/overview.html @@ -0,0 +1,22 @@ + + +This document is the API specification for Spring Integration +
+
+

+ For further API reference and developer documentation, see the + Spring + Integration reference documentation. + That documentation contains more detailed, developer-targeted + descriptions, with conceptual overviews, definitions of terms, + workarounds, and working code examples. +

+ +

+ If you are interested in commercial training, consultancy, and + support for Spring Integration, please visit + http://www.springsource.com +

+
+ + diff --git a/spring-integration-zip/src/dist/changelog.txt b/spring-integration-zip/src/dist/changelog.txt new file mode 100644 index 0000000..d263567 --- /dev/null +++ b/spring-integration-zip/src/dist/changelog.txt @@ -0,0 +1,15 @@ +Spring Integration Zip Adapter CHANGELOG +========================================= + +For the full detailed changelog, see: +https://.... + + +Changes in version 1.0 GA (insert date here) +https://.... + + +*** GENERAL *** + +Upgraded Spring Framework dependency to ... +... diff --git a/spring-integration-zip/src/dist/license.txt b/spring-integration-zip/src/dist/license.txt new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/spring-integration-zip/src/dist/license.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + http://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. diff --git a/spring-integration-zip/src/dist/notice.txt b/spring-integration-zip/src/dist/notice.txt new file mode 100644 index 0000000..f62045a --- /dev/null +++ b/spring-integration-zip/src/dist/notice.txt @@ -0,0 +1,21 @@ + ======================================================================== + == NOTICE file corresponding to section 4 d of the Apache License, == + == Version 2.0, in this case for the Spring Integration distribution. == + ======================================================================== + + This product includes software developed by + the Apache Software Foundation (http://www.apache.org). + + The end-user documentation included with a redistribution, if any, + must include the following acknowledgement: + + "This product includes software developed by the Spring Framework + Project (http://www.springframework.org)." + + Alternatively, this acknowledgement may appear in the software itself, + if and wherever such third-party acknowledgements normally appear. + + The names "Spring", "Spring Framework", and "Spring Integration" must + not be used to endorse or promote products derived from this software + without prior written permission. For written permission, please contact + enquiries@springsource.com. diff --git a/spring-integration-zip/src/dist/readme.txt b/spring-integration-zip/src/dist/readme.txt new file mode 100644 index 0000000..c863ba6 --- /dev/null +++ b/spring-integration-zip/src/dist/readme.txt @@ -0,0 +1,13 @@ +Spring Integration Zip Adapter +----------------------------------- + +To find out what has changed since any earlier releases, see 'changelog.txt'. + +Please consult the documentation located within the 'docs/reference' directory +of this release and also visit the official Spring Integration home at +http://www.springsource.org/spring-integration + +There you will find links to the forum, issue tracker, and several other resources. + +See https://github.com/SpringSource/spring-integration#readme for additional +information including instructions on building from source. 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 0000000..1b1fe01 --- /dev/null +++ b/spring-integration-zip/src/main/java/org/springframework/integration/zip/ZipHeaders.java @@ -0,0 +1,37 @@ +/* + * Copyright 2015 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 + * + * http://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 1.0 + */ +public final class ZipHeaders { + + private 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"; + + /** Noninstantiable utility class */ + private ZipHeaders() { + throw new AssertionError(); + } + +} 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 0000000..578c71b --- /dev/null +++ b/spring-integration-zip/src/main/java/org/springframework/integration/zip/config/xml/AbstractZipTransformerParser.java @@ -0,0 +1,58 @@ +/* + * Copyright 2015 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 + * + * http://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.util.StringUtils; + +/** + * Base class for Zip transformer parsers. + * + * @author Gunnar Hillert + * @since 1.0 + */ +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); + } + this.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 0000000..d7ef757 --- /dev/null +++ b/spring-integration-zip/src/main/java/org/springframework/integration/zip/config/xml/UnZipTransformerParser.java @@ -0,0 +1,65 @@ +/* + * Copyright 2015 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 + * + * http://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.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; +import org.springframework.integration.zip.transformer.ZipResultType; +import org.springframework.util.StringUtils; +import org.w3c.dom.Element; + +/** + * Parser for the 'unzip-transformer' element. + * + * @author Gunnar Hillert + * @since 1.0 + */ +public class UnZipTransformerParser extends AbstractZipTransformerParser { + + @Override + protected String getTransformerClassName() { + return UnZipTransformer.class.getName(); + } + + @Override + protected void postProcessTransformer(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + Object source = parserContext.extractSource(element); + + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "charset"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "expect-single-result"); + + final String resultType = element.getAttribute("result-type"); + + if (StringUtils.hasText(resultType)) { + + final ZipResultType zipResultType = ZipResultType.convertToZipResultType(resultType); + + if (zipResultType != null) { + builder.addPropertyValue("zipResultType", zipResultType); + } + else { + parserContext.getReaderContext().error( + String.format("Unable to convert the provided result-type '%s' " + + "to the respective ZipResultType enum.", resultType), source); + } + + } + } + +} 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 0000000..6ef92e5 --- /dev/null +++ b/spring-integration-zip/src/main/java/org/springframework/integration/zip/config/xml/ZipNamespaceHandler.java @@ -0,0 +1,38 @@ +/* + * Copyright 2015 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 + * + * http://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 1.0 + * + */ +public class ZipNamespaceHandler extends AbstractIntegrationNamespaceHandler { + + /* (non-Javadoc) + * @see org.springframework.beans.factory.xml.NamespaceHandler#init() + */ + @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 0000000..45d9b96 --- /dev/null +++ b/spring-integration-zip/src/main/java/org/springframework/integration/zip/config/xml/ZipTransformerParser.java @@ -0,0 +1,65 @@ +/* + * Copyright 2015 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 + * + * http://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.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.integration.config.xml.IntegrationNamespaceUtils; +import org.springframework.integration.zip.transformer.ZipResultType; +import org.springframework.integration.zip.transformer.ZipTransformer; +import org.springframework.util.StringUtils; +import org.w3c.dom.Element; + +/** + * Parser for the 'zip-transformer' element. + * + * @author Gunnar Hillert + * @since 1.0 + */ +public class ZipTransformerParser extends AbstractZipTransformerParser { + + @Override + protected String getTransformerClassName() { + return ZipTransformer.class.getName(); + } + + @Override + protected void postProcessTransformer(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + Object source = parserContext.extractSource(element); + + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "charset"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "compression-level"); + + final String resultType = element.getAttribute("result-type"); + + if (StringUtils.hasText(resultType)) { + + final ZipResultType zipResultType = ZipResultType.convertToZipResultType(resultType); + + if (zipResultType != null) { + builder.addPropertyValue("zipResultType", zipResultType); + } + else { + parserContext.getReaderContext().error( + String.format("Unable to convert the provided result-type '%s' " + + "to the respective ZipResultType enum.", resultType), source); + } + + } + } + +} 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 0000000..e23dd6f --- /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 0000000..96d4adb --- /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/transformer/AbstractZipTransformer.java b/spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/AbstractZipTransformer.java new file mode 100644 index 0000000..c32f411 --- /dev/null +++ b/spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/AbstractZipTransformer.java @@ -0,0 +1,131 @@ +/* + * Copyright 2015 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 + * + * http://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.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +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 + * @since 1.0 + */ +public abstract class AbstractZipTransformer extends AbstractTransformer { + + private static final Log logger = LogFactory.getLog(ZipTransformer.class); + + protected volatile Charset charset = Charset.defaultCharset(); + + protected volatile FileNameGenerator fileNameGenerator; + + protected volatile ZipResultType zipResultType = ZipResultType.FILE; + + protected volatile File workDirectory = new File(System.getProperty("java.io.tmpdir") + File.separator + "ziptransformer"); + + protected volatile 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 sub-directory "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.isFile(), "The workDirectory specified must not point to a file"); + this.workDirectory = workDirectory; + } + + /** + * Defines 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() throws Exception { + super.onInit(); + + if (!workDirectory.exists()) { + if (logger.isInfoEnabled()) { + logger.info(String.format("Creating work directory '%s'.", this.workDirectory)); + } + workDirectory.mkdirs(); + } + final 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) throws Exception { + Assert.notNull(message, "message must not be null"); + final Object payload = message.getPayload(); + Assert.notNull(payload, "payload must not be null"); + + 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. + * @throws Exception Any exception. + */ + protected abstract Object doZipTransform(Message message) throws Exception; + +} 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 0000000..f1f2539 --- /dev/null +++ b/spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/SpringZipUtils.java @@ -0,0 +1,149 @@ +/* + * Copyright 2015 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 + * + * http://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.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.zeroturnaround.zip.ZipEntrySource; +import org.zeroturnaround.zip.ZipException; + +/** + * 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 1.0 + * + */ +public class SpringZipUtils { + + private static final Log logger = LogFactory.getLog(SpringZipUtils.class); + + public static byte[] pack(Collection entries, int compressionLevel) { + + if (logger.isDebugEnabled()) { + logger.debug(String.format("Creating byte array from '%s'.", + entries)); + } + + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + pack(entries, outputStream, compressionLevel); + + return outputStream.toByteArray(); + } + + public static void pack(Collection entries, File zip, + int compressionLevel) { + + if (logger.isDebugEnabled()) { + logger.debug("Creating '" + zip + "' from " + + entries + "."); + } + + final 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 { + try { + if (zipfile != null) { + zipfile.close(); + zipfile = null; + } + } catch (IOException e) { + } + } + } +} 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 0000000..c8ec135 --- /dev/null +++ b/spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/UnZipTransformer.java @@ -0,0 +1,190 @@ +/* + * Copyright 2015 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 + * + * http://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.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHandlingException; +import org.springframework.messaging.MessagingException; +import org.zeroturnaround.zip.ZipEntryCallback; +import org.zeroturnaround.zip.ZipUtil; + +/** + * Transformer implementation that applies an UnZip transformation to the message + * payload. + * + * @author Gunnar Hillert + * @since 1.0 + * + */ +public class UnZipTransformer extends AbstractZipTransformer { + + private static final Log logger = LogFactory.getLog(UnZipTransformer.class); + + 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) throws Exception { + + try { + final Object payload = message.getPayload(); + final Object unzippedData; + + InputStream inputStream = null; + + try { + if (payload instanceof File) { + final File filePayload = (File) payload; + + if (filePayload.isDirectory()) { + throw new UnsupportedOperationException(String.format("Cannot unzip a directory: '%s'", filePayload.getAbsolutePath())); + } + + if (!SpringZipUtils.isValid(filePayload)) { + throw new IllegalStateException(String.format("Not a zip file: '%s'.", 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(String.format("Unsupported payload type '%s'. The only supported payload types are " + + "java.io.File, byte[] and java.io.InputStream", payload.getClass().getSimpleName())); + } + + 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"; + + if (logger.isWarnEnabled()) { + logger.warn(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 tempDir = new File(workDirectory, message.getHeaders().getId().toString()); + tempDir.mkdirs(); + final File destinationFile = new File(tempDir, zipEntryName); + + if (zipEntry.isDirectory()) { + destinationFile.mkdirs(); + } + else { + SpringZipUtils.copy(zipEntryInputStream, destinationFile); + uncompressedData.put(zipEntryName, destinationFile); + } + } + else if (ZipResultType.BYTE_ARRAY.equals(zipResultType)) { + if (!zipEntry.isDirectory()) { + byte[] data = IOUtils.toByteArray(zipEntryInputStream); + uncompressedData.put(zipEntryName, data); + } + } + else { + throw new IllegalStateException("Unsupported zipResultType " + zipResultType); + } + } + }); + + if (uncompressedData.isEmpty()) { + if (logger.isWarnEnabled()) { + 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; + } + + } + + if (payload instanceof File && this.deleteFiles) { + final File filePayload = (File) payload; + if (!filePayload.delete() && logger.isWarnEnabled()) { + if (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); + } + } +} 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 0000000..5583c74 --- /dev/null +++ b/spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/ZipResultType.java @@ -0,0 +1,62 @@ +/* + * Copyright 2015 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 + * + * http://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 org.springframework.util.Assert; + +/** + * + * @author Gunnar Hillert + * @since 1.0 + */ +public enum ZipResultType { + + FILE("FILE"), + BYTE_ARRAY("BYTE_ARRAY"); + + private String id; + + private ZipResultType(String id) { + this.id = id; + } + + /** + * Retrieves the matching enum constant for a provided String representation + * of the {@link ZipResultType}. The provided name must match exactly the identifier as + * used to declare the enum constant. + * + * @param zipResultTypeAsString Name of the enum to convert. Must be not null and not empty. + * @return The enumeration that matches. Returns Null of no match was found. + * + */ + public static ZipResultType convertToZipResultType(String zipResultTypeAsString) { + + Assert.hasText(zipResultTypeAsString, "Parameter zipResultTypeAsString must not be null or empty"); + + for (ZipResultType zipResultType : ZipResultType.values()) { + if (zipResultType.name().equalsIgnoreCase(zipResultTypeAsString)) { + return zipResultType; + } + } + + return null; + } + + public String getId() { + return id; + } + +} 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 0000000..f0d124f --- /dev/null +++ b/spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/ZipTransformer.java @@ -0,0 +1,229 @@ +/* + * Copyright 2015 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 + * + * http://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.util.ArrayList; +import java.util.Date; +import java.util.zip.Deflater; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +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.messaging.MessageHandlingException; +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StringUtils; +import org.zeroturnaround.zip.ByteSource; +import org.zeroturnaround.zip.FileSource; +import org.zeroturnaround.zip.ZipEntrySource; + +/** + * {@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: http://mindprod.com/jgloss/zip.html + * + * If you want to generate Zip files larger than {@code 4GB}, you must use Java 7: + * + * See also: https://blogs.oracle.com/xuemingshen/entry/zip64_support_for_4g_zipfile + * + * @author Gunnar Hillert + * @since 1.0 + * + */ +public class ZipTransformer extends AbstractZipTransformer { + + private static final Log logger = LogFactory.getLog(ZipTransformer.class); + + private static final String ZIP_EXTENSION = ".zip"; + + private volatile int compressionLevel = Deflater.DEFAULT_COMPRESSION; + + private volatile boolean useFileAttributes = true; + + @Autowired Environment env; + + /** + * Sets 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; + } + + /** + * Specifies 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: + * + *
    + *
  • {@link File} + *...
  • {@link String} + *...
  • byte[] + *...
  • {@link Iterable} + *
+ * + * When providing an {@link Iterable}, nested Iterables are not supported. However, + * payloads can be of of any of the other supported types. + * + */ + @Override + protected Object doZipTransform(Message message) throws Exception { + + try { + + 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 = baseFileName; + } + 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, useFileAttributes); + if (logger.isDebugEnabled()) { + logger.debug("ZipEntrySource path: '" + zipEntrySource.getPath() + "'"); + } + entries.add(zipEntrySource); + counter++; + } + } + else { + final ZipEntrySource zipEntrySource = createZipEntrySource(payload, lastModifiedDate, zipEntryName, useFileAttributes); + entries.add(zipEntrySource); + } + + final byte[] zippedBytes = SpringZipUtils.pack(entries, this.compressionLevel); + + if (ZipResultType.FILE.equals(zipResultType)) { + final File zippedFile = new File(this.workDirectory, zipFileName); + FileCopyUtils.copy(zippedBytes, zippedFile); + zippedData = zippedFile; + } + else if (ZipResultType.BYTE_ARRAY.equals(zipResultType)) { + zippedData = zippedBytes; + } + else { + throw new IllegalStateException("Unsupported zipResultType " + zipResultType); + } + + return this.getMessageBuilderFactory() + .withPayload(zippedData).copyHeaders(message.getHeaders()).setHeader(FileHeaders.FILENAME, zipFileName).build(); + } + catch (Exception e) { + throw new MessageHandlingException(message, "Failed to apply Zip transformation.", e); + } + } + + private ZipEntrySource createZipEntrySource(Object item, + Date lastModifiedDate, String zipEntryName, boolean useFileAttributes) { + + if (item instanceof File) { + final File filePayload = (File) item; + + final String fileName = useFileAttributes ? filePayload.getName() : zipEntryName; + + if (((File) item).isDirectory()) { + throw new UnsupportedOperationException("Zipping of directories is not supported."); + } + + final FileSource fileSource = new FileSource(fileName, filePayload); + + if (this.deleteFiles) { + if (!filePayload.delete() && logger.isWarnEnabled()) { + logger.warn("failed to delete File '" + filePayload + "'"); + } + } + + return fileSource; + + } + else if (item instanceof byte[] || item instanceof String) { + + byte[] bytesToCompress = null; + + if (item instanceof String) { + bytesToCompress = ((String) item).getBytes(this.charset); + } + else { + bytesToCompress = (byte[]) item; + } + + final ZipEntrySource zipEntrySource = new ByteSource(zipEntryName, bytesToCompress, lastModifiedDate.getTime()); + + return zipEntrySource; + } + 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/splitter/UnZipResultSplitter.java b/spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/splitter/UnZipResultSplitter.java new file mode 100644 index 0000000..3c50853 --- /dev/null +++ b/spring-integration-zip/src/main/java/org/springframework/integration/zip/transformer/splitter/UnZipResultSplitter.java @@ -0,0 +1,52 @@ +/* + * Copyright 2015 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 + * + * http://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.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.support.MessageBuilder; +import org.springframework.integration.zip.ZipHeaders; +import org.springframework.messaging.Message; + +/** + * + * @author Gunnar Hillert + * @since 1.0 + * + */ +public class UnZipResultSplitter { + + public List> splitUnzippedMap(Map unzippedEntries) { + + final List> messages = new ArrayList>(unzippedEntries.size()); + + for (Map.Entry entry : unzippedEntries.entrySet()) { + final String path = FilenameUtils.getPath(entry.getKey()); + final String filename = FilenameUtils.getName(entry.getKey()); + final Message splitMessage = MessageBuilder.withPayload(entry.getValue()) + .setHeader(FileHeaders.FILENAME, filename) + .setHeader(ZipHeaders.ZIP_ENTRY_PATH, path).build(); + messages.add(splitMessage); + } + return messages; + } + +} 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 0000000..fb31de6 --- /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 0000000..5261bf2 --- /dev/null +++ b/spring-integration-zip/src/main/resources/META-INF/spring.schemas @@ -0,0 +1,2 @@ +http\://www.springframework.org/schema/integration/zip/spring-integration-zip-1.0.xsd=org/springframework/integration/config/xml/spring-integration-zip-1.0.xsd +http\://www.springframework.org/schema/integration/zip/spring-integration-zip.xsd=org/springframework/integration/config/xml/spring-integration-zip-1.0.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 0000000..c8c2bdc --- /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/config/xml/spring-integration-zip.gif diff --git a/spring-integration-zip/src/main/resources/org/springframework/integration/config/xml/spring-integration-zip-1.0.xsd b/spring-integration-zip/src/main/resources/org/springframework/integration/config/xml/spring-integration-zip-1.0.xsd new file mode 100644 index 0000000..f39ad96 --- /dev/null +++ b/spring-integration-zip/src/main/resources/org/springframework/integration/config/xml/spring-integration-zip-1.0.xsd @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + 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 uncompresses message + payloads using Zip compressions. The following payload types are + supported: + + - java.io.File + - byte[] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/main/resources/org/springframework/integration/config/xml/spring-integration-zip.gif b/spring-integration-zip/src/main/resources/org/springframework/integration/config/xml/spring-integration-zip.gif new file mode 100644 index 0000000000000000000000000000000000000000..210e0764fa4c1e5baebcdea156bc3f3e1f97a2c9 GIT binary patch literal 539 zcmZ?wbhEHb6krfwc*Xz%|NsB*n7VH6iaomzUp%y<^XRV5le@dm?CrjBr0?SXo|o4a z>{}Ri^GM%=bBkWz+y8vM?x%h3?+zusxj5zR@#^Jm-pe}!R`mow*jzjz+p;&)qBX&& zG1jmr&Ac*H`_zJhTu;?PU-d#?%_oZ!?=4DAaa1VqRO?H!xIQD{=A4w7g+5uX8WR(2 zrX<_tc8Bb5QRHvpiB8JU`8KTWP?thKQZD zFcaE=iuVx<>PO@dH?gn{*FclYXGfDn1BEP literal 0 HcmV?d00001 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 0000000..fb21f1e --- /dev/null +++ b/spring-integration-zip/src/test/java/org/springframework/integration/zip/UnZip2FileTests-context.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + 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 0000000..3d4d14c --- /dev/null +++ b/spring-integration-zip/src/test/java/org/springframework/integration/zip/UnZip2FileTests.java @@ -0,0 +1,160 @@ +/* + * Copyright 2015 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 + * + * http://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.io.InputStream; +import java.util.Properties; + +import org.apache.commons.io.IOUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; + +/** + * + * @author Gunnar Hillert + * @since 1.0 + * + */ +public class UnZip2FileTests { + + private AnnotationConfigApplicationContext context; + private ResourceLoader resourceLoader; + private MessageChannel input; + + private static final Properties properties = new Properties(); + + @Rule + public TemporaryFolder testFolder = new TemporaryFolder(); + + private File workDir; + + @Before + public void setup() throws IOException { + this.workDir = testFolder.newFolder(); + properties.put("workDir", workDir); + System.out.print(this.workDir.getAbsolutePath()); + + context = new AnnotationConfigApplicationContext(); + context.register(ContextConfiguration.class); + context.refresh(); + input = context.getBean("input", MessageChannel.class); + resourceLoader = context; + } + + @After + public void cleanup() { + if (context != null) { + context.close(); + } + } + + @Test + @Ignore + public void unZipWithOneEntry() throws Exception { + + final Resource resource = resourceLoader.getResource("classpath:testzipdata/single.zip"); + final InputStream is = resource.getInputStream(); + + byte[] zipdata = IOUtils.toByteArray(is); + + final Message message = MessageBuilder.withPayload(zipdata).build(); + + input.send(message); + + Assert.assertTrue(this.workDir.list().length == 1); + + File fileInWorkDir = this.workDir.listFiles()[0]; + + Assert.assertTrue(fileInWorkDir.isFile()); + Assert.assertEquals("single.txt", fileInWorkDir.getName()); + } + + @Test + public void unZipWithMultipleEntries() throws Exception { + + final Resource resource = resourceLoader.getResource("classpath:testzipdata/countries.zip"); + final InputStream is = resource.getInputStream(); + + byte[] zipdata = IOUtils.toByteArray(is); + + final Message message = MessageBuilder.withPayload(zipdata).build(); + + input.send(message); + + Assert.assertTrue(this.workDir.list().length == 4); + + File[] files = this.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; + Assert.assertTrue(file.isDirectory()); + Assert.assertTrue(file.list().length == 2); + } + if (file.getName().equals("de.txt")) { + de = true; + Assert.assertTrue(file.isFile()); + } + if (file.getName().equals("fr.txt")) { + fr = true; + Assert.assertTrue(file.isFile()); + } + if (file.getName().equals("pl.txt")) { + pl = true; + Assert.assertTrue(file.isFile()); + } + } + + Assert.assertTrue(continents); + Assert.assertTrue(de); + Assert.assertTrue(fr); + Assert.assertTrue(pl); + + } + + @Configuration + @ImportResource("classpath:org/springframework/integration/zip/UnZip2FileTests-context.xml") + public static class ContextConfiguration { + + @Bean + Properties properties() throws IOException { + return properties; + } + + } +} 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 0000000..c348a6c --- /dev/null +++ b/spring-integration-zip/src/test/java/org/springframework/integration/zip/Zip2FileTests-context.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + 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 0000000..74eeb13 --- /dev/null +++ b/spring-integration-zip/src/test/java/org/springframework/integration/zip/Zip2FileTests.java @@ -0,0 +1,175 @@ +/* + * Copyright 2015 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 + * + * http://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.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import org.apache.commons.io.FileUtils; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportResource; +import org.springframework.integration.file.FileHeaders; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; + +/** + * + * @author Gunnar Hillert + * @since 1.0 + * + */ +public class Zip2FileTests { + + private AnnotationConfigApplicationContext context; + private MessageChannel input; + + private static final Properties properties = new Properties(); + + @Rule + public TemporaryFolder testFolder = new TemporaryFolder(); + + private File workDir; + + @Before + public void setup() throws IOException { + this.workDir = testFolder.newFolder(); + properties.put("workDir", workDir); + System.out.print(this.workDir.getAbsolutePath()); + + context = new AnnotationConfigApplicationContext(); + context.register(ContextConfiguration.class); + context.refresh(); + input = context.getBean("input", MessageChannel.class); + } + + @After + public void cleanup() { + if (context != null) { + context.close(); + } + } + + @Test + public void zipStringWithDefaultFileName() throws FileNotFoundException, IOException, InterruptedException { + + final Message message = MessageBuilder.withPayload("Zip me up.").build(); + + input.send(message); + + Assert.assertTrue(this.workDir.list().length == 1); + + File fileInWorkDir = this.workDir.listFiles()[0]; + + Assert.assertTrue(fileInWorkDir.isFile()); + Assert.assertTrue(fileInWorkDir.getName().contains(message.getHeaders().getId().toString())); + Assert.assertTrue("The created file should have a 'zip' file extension.", fileInWorkDir.getName().endsWith(".zip")); + } + + @Test + public void zipStringWithExplicitFileName() throws FileNotFoundException, IOException, InterruptedException { + input.send(MessageBuilder.withPayload("Zip me up.").setHeader(FileHeaders.FILENAME, "zipString.zip").build()); + + Assert.assertTrue(this.workDir.list().length == 1); + Assert.assertEquals("zipString.zip", this.workDir.listFiles()[0].getName()); + } + + @Test + public void zipBytesWithExplicitFileName() throws FileNotFoundException, IOException, InterruptedException { + + input.send(MessageBuilder.withPayload("Zip me up.".getBytes()).setHeader(FileHeaders.FILENAME, "zipString.zip").build()); + + Assert.assertTrue(this.workDir.list().length == 1); + Assert.assertEquals("zipString.zip", this.workDir.listFiles()[0].getName()); + } + + @Test + public void zipFile() throws FileNotFoundException, IOException, InterruptedException { + + final File fileToCompress = testFolder.newFile(); + FileUtils.writeStringToFile(fileToCompress, "hello world"); + + input.send(MessageBuilder.withPayload(fileToCompress).build()); + + Assert.assertTrue(this.workDir.list().length == 1); + Assert.assertEquals(fileToCompress.getName() + ".zip", this.workDir.listFiles()[0].getName()); + } + + @Test + public void zipIterableWithMultipleStrings() throws FileNotFoundException, IOException, InterruptedException { + + String stringToCompress1 = "String1"; + String stringToCompress2 = "String2"; + String stringToCompress3 = "String3"; + String stringToCompress4 = "String4"; + + final 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()); + + Assert.assertTrue(this.workDir.list().length == 1); + Assert.assertEquals("zipWith4Strings.zip", this.workDir.listFiles()[0].getName()); + } + + @Test + public void zipIterableWithDifferentTypes() throws FileNotFoundException, IOException, InterruptedException { + + String stringToCompress = "String1"; + byte[] bytesToCompress = "String2".getBytes(); + final File fileToCompress = testFolder.newFile(); + FileUtils.writeStringToFile(fileToCompress, "hello world"); + + 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()); + + Assert.assertTrue(this.workDir.list().length == 1); + Assert.assertEquals("objects-to-compress.zip", this.workDir.listFiles()[0].getName()); + } + + @Configuration + @ImportResource("classpath:org/springframework/integration/zip/Zip2FileTests-context.xml") + public static class ContextConfiguration { + + @Bean + Properties properties() throws IOException { + return properties; + } + + } +} 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 0000000..179e8e8 --- /dev/null +++ b/spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/UnZipTransformerParserTests.java @@ -0,0 +1,140 @@ +/* + * Copyright 2015 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 + * + * http://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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.nio.charset.Charset; + +import org.junit.After; +import org.junit.Test; +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.UnZipTransformer; +import org.springframework.integration.zip.transformer.ZipResultType; +import org.springframework.util.Assert; + +/** + * + * @author Gunnar Hillert + * @since 1.0 + * + */ +public class UnZipTransformerParserTests { + + private ConfigurableApplicationContext context; + + @Test + public void testUnZiptransformerParserWithDefaults() { + + setUp("UnZipTransformerParserTests.xml", getClass()); + + EventDrivenConsumer consumer = this.context.getBean("unzipTransformerWithDefaults", EventDrivenConsumer.class); + + final AbstractMessageChannel inputChannel = TestUtils.getPropertyValue(consumer, "inputChannel", AbstractMessageChannel.class); + assertEquals("input", inputChannel.getComponentName()); + + final MessageTransformingHandler handler = TestUtils.getPropertyValue(consumer, "handler", MessageTransformingHandler.class); + + final AbstractMessageChannel outputChannel = TestUtils.getPropertyValue(handler, "outputChannel", AbstractMessageChannel.class); + assertEquals("output", outputChannel.getComponentName()); + + 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); + + assertNotNull(charset); + assertNotNull(fileNameGenerator); + assertNotNull(zipResultType); + assertNotNull(workDirectory); + assertNotNull(deleteFiles); + assertNotNull(expectSingleResult); + + assertEquals(Charset.defaultCharset(), charset); + Assert.isInstanceOf(DefaultFileNameGenerator.class, fileNameGenerator); + assertEquals(ZipResultType.FILE, zipResultType); + assertEquals(new File(System.getProperty("java.io.tmpdir") + File.separator + "ziptransformer"), workDirectory); + assertTrue("WorkDirectory should exist.", workDirectory.exists()); + assertTrue("WorkDirectory should be a directory.", workDirectory.isDirectory()); + assertFalse("By default the 'deleteFiles' property should be false.", deleteFiles); + assertFalse("The 'expectSingleResult' property should be false.", expectSingleResult); + } + + @Test + public void testUnZiptransformerParserWithExplicitSettings() { + + setUp("UnZipTransformerParserTests.xml", getClass()); + + EventDrivenConsumer consumer = this.context.getBean("unzipTransformer", EventDrivenConsumer.class); + + final AbstractMessageChannel inputChannel = TestUtils.getPropertyValue(consumer, "inputChannel", AbstractMessageChannel.class); + assertEquals("input", inputChannel.getComponentName()); + + final MessageTransformingHandler handler = TestUtils.getPropertyValue(consumer, "handler", MessageTransformingHandler.class); + + final AbstractMessageChannel outputChannel = TestUtils.getPropertyValue(handler, "outputChannel", AbstractMessageChannel.class); + assertEquals("output", outputChannel.getComponentName()); + + 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); + + assertNotNull(charset); + assertNotNull(fileNameGenerator); + assertNotNull(zipResultType); + assertNotNull(workDirectory); + assertNotNull(deleteFiles); + assertNotNull(expectSingleResult); + + assertEquals(Charset.defaultCharset(), charset); + Assert.isInstanceOf(DefaultFileNameGenerator.class, fileNameGenerator); + assertEquals(ZipResultType.FILE, zipResultType); + assertEquals(new File(System.getProperty("java.io.tmpdir") + File.separator + "ziptransformer"), workDirectory); + assertTrue("WorkDirectory should exist.", workDirectory.exists()); + assertTrue("WorkDirectory should be a directory.", workDirectory.isDirectory()); + assertTrue("The 'deleteFiles' property should be true.", deleteFiles); + assertTrue("The 'expectSingleResult' property should be true.", expectSingleResult); + } + + @After + public void tearDown(){ + if(context != null){ + context.close(); + } + } + + public void setUp(String name, Class cls){ + context = new ClassPathXmlApplicationContext(name, cls); + } + +} diff --git a/spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/UnZipTransformerParserTests.xml b/spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/UnZipTransformerParserTests.xml new file mode 100644 index 0000000..167565f --- /dev/null +++ b/spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/UnZipTransformerParserTests.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 0000000..bcc3f6a --- /dev/null +++ b/spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/ZipTransformerParserTests.java @@ -0,0 +1,159 @@ +/* + * Copyright 2015 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 + * + * http://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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.File; +import java.nio.charset.Charset; +import java.util.zip.Deflater; + +import org.junit.After; +import org.junit.Test; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +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.util.Assert; + +/** + * + * @author Gunnar Hillert + * @since 1.0 + * + */ +public class ZipTransformerParserTests { + + private ConfigurableApplicationContext context; + + @Test + public void testZiptransformerParserWithDefaults() { + + setUp("ZipTransformerParserTests.xml", getClass()); + + EventDrivenConsumer consumer = this.context.getBean("zipTransformerWithDefaults", EventDrivenConsumer.class); + + final AbstractMessageChannel inputChannel = TestUtils.getPropertyValue(consumer, "inputChannel", AbstractMessageChannel.class); + assertEquals("input", inputChannel.getComponentName()); + + final MessageTransformingHandler handler = TestUtils.getPropertyValue(consumer, "handler", MessageTransformingHandler.class); + + final AbstractMessageChannel outputChannel = TestUtils.getPropertyValue(handler, "outputChannel", AbstractMessageChannel.class); + assertEquals("output", outputChannel.getComponentName()); + + 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); + + assertNotNull(charset); + assertNotNull(fileNameGenerator); + assertNotNull(zipResultType); + assertNotNull(workDirectory); + assertNotNull(compressionLevel); + assertNotNull(deleteFiles); + + assertEquals(Charset.defaultCharset(), charset); + Assert.isInstanceOf(DefaultFileNameGenerator.class, fileNameGenerator); + assertEquals(ZipResultType.FILE, zipResultType); + assertEquals(new File(System.getProperty("java.io.tmpdir") + File.separator + "ziptransformer"), workDirectory); + assertTrue("WorkDirectory should exist.", workDirectory.exists()); + assertTrue("WorkDirectory should be a directory.", workDirectory.isDirectory()); + assertEquals(Integer.valueOf(Deflater.DEFAULT_COMPRESSION), Integer.valueOf(compressionLevel)); + assertFalse("By default the 'deleteFiles' property should be false.", deleteFiles); + } + + @Test + public void testZiptransformerParserWithExplicitSettings() { + + setUp("ZipTransformerParserTests.xml", getClass()); + + EventDrivenConsumer consumer = this.context.getBean("zipTransformer", EventDrivenConsumer.class); + + final AbstractMessageChannel inputChannel = TestUtils.getPropertyValue(consumer, "inputChannel", AbstractMessageChannel.class); + assertEquals("input", inputChannel.getComponentName()); + + final MessageTransformingHandler handler = TestUtils.getPropertyValue(consumer, "handler", MessageTransformingHandler.class); + + final AbstractMessageChannel outputChannel = TestUtils.getPropertyValue(handler, "outputChannel", AbstractMessageChannel.class); + assertEquals("output", outputChannel.getComponentName()); + + 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); + + assertNotNull(charset); + assertNotNull(fileNameGenerator); + assertNotNull(zipResultType); + assertNotNull(workDirectory); + assertNotNull(compressionLevel); + assertNotNull(deleteFiles); + + assertEquals(Charset.defaultCharset(), charset); + Assert.isInstanceOf(DefaultFileNameGenerator.class, fileNameGenerator); + assertEquals(ZipResultType.BYTE_ARRAY, zipResultType); + assertEquals(new File(System.getProperty("java.io.tmpdir") + File.separator + "ziptransformer"), workDirectory); + assertTrue("WorkDirectory should exist.", workDirectory.exists()); + assertTrue("WorkDirectory should be a directory.", workDirectory.isDirectory()); + assertEquals(Integer.valueOf(2), Integer.valueOf(compressionLevel)); + assertTrue("The 'deleteFiles' property should be true.", deleteFiles); + } + + @Test + public void testZiptransformerParserWithIncorrectResultType() { + + try { + setUp("ZipTransformerParserTestsWithIncorrectResultType.xml", getClass()); + } + catch (BeanDefinitionParsingException e) { + String expectedErrorMessage = "Unable to convert the provided result-type 'INCORRECT' " + + "to the respective ZipResultType enum."; + assertTrue(String.format("Expected exception message to contain '%s' but got '%s'", expectedErrorMessage, e.getMessage()), + e.getMessage().contains(expectedErrorMessage)); + return; + } + + fail("Expected a BeanDefinitionParsingException to be thrown."); + } + @After + public void tearDown(){ + if(context != null){ + context.close(); + } + } + + public void setUp(String name, Class cls){ + context = new ClassPathXmlApplicationContext(name, cls); + } + +} diff --git a/spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/ZipTransformerParserTests.xml b/spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/ZipTransformerParserTests.xml new file mode 100644 index 0000000..c98ebbe --- /dev/null +++ b/spring-integration-zip/src/test/java/org/springframework/integration/zip/config/xml/ZipTransformerParserTests.xml @@ -0,0 +1,19 @@ + + + + + + + + + + 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 0000000..e20dc3d --- /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/transformer/UnZipTransformerTests.java b/spring-integration-zip/src/test/java/org/springframework/integration/zip/transformer/UnZipTransformerTests.java new file mode 100644 index 0000000..42919ae --- /dev/null +++ b/spring-integration-zip/src/test/java/org/springframework/integration/zip/transformer/UnZipTransformerTests.java @@ -0,0 +1,253 @@ +/* + * Copyright 2015 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 + * + * http://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.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessagingException; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * + * @author Gunnar Hillert + * @since 1.0 + * + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration({"classpath:org/springframework/integration/zip/transformer/UnZipTransformerTests.xml"}) +public class UnZipTransformerTests { + + @Rule + public TemporaryFolder testFolder = new TemporaryFolder(); + + @Autowired + private ResourceLoader resourceLoader; + + private File workDir; + + @Before + public void setup() throws IOException { + this.workDir = testFolder.newFolder(); + } + + /** + * UnCompress a ZIP archive containing a single file only. The result will be + * a byte array. + * + * @throws IOException + */ + @Test + public void unzipSingleFileAsInputstreamToByteArray() throws IOException { + + final Resource resource = 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); + + Assert.assertNotNull(resultMessage); + + @SuppressWarnings("unchecked") + Map unzippedData = (Map) resultMessage.getPayload(); + + Assert.assertNotNull(unzippedData); + Assert.assertTrue(unzippedData.size() == 1); + Assert.assertEquals("Spring Integration Rocks!", new String(unzippedData.values().iterator().next())); + + } + + /** + * + * + * @throws IOException + */ + @Test + public void unzipSingleFileToByteArray() throws IOException { + + final Resource resource = resourceLoader.getResource("classpath:testzipdata/single.zip"); + final InputStream is = resource.getInputStream(); + + final File inputFile = new File(this.workDir, "unzipSingleFileToByteArray"); + + IOUtils.copy(is, new FileOutputStream(inputFile)); + + 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); + + Assert.assertNotNull(resultMessage); + + @SuppressWarnings("unchecked") + Map unzippedData = (Map) resultMessage.getPayload(); + + Assert.assertNotNull(unzippedData); + Assert.assertTrue(unzippedData.size() == 1); + Assert.assertTrue(inputFile.exists()); + Assert.assertEquals("Spring Integration Rocks!", new String(unzippedData.values().iterator().next())); + + } + + /** + * + * + * @throws IOException + */ + @Test + public void unzipSingleFileToByteArrayWithDeleteFilesTrue() throws IOException { + + final Resource resource = resourceLoader.getResource("classpath:testzipdata/single.zip"); + final InputStream is = resource.getInputStream(); + + final File inputFile = new File(this.workDir, "unzipSingleFileToByteArray"); + + IOUtils.copy(is, new FileOutputStream(inputFile)); + + 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); + + Assert.assertNotNull(resultMessage); + + @SuppressWarnings("unchecked") + Map unzippedData = (Map) resultMessage.getPayload(); + + Assert.assertNotNull(unzippedData); + Assert.assertTrue(unzippedData.size() == 1); + Assert.assertFalse(inputFile.exists()); + Assert.assertEquals("Spring Integration Rocks!", new String(unzippedData.values().iterator().next())); + + } + + /** + * UnCompress a ZIP archive containing multiple files. The result will be + * a collection of files. + * + * @throws IOException + */ + @Test + public void unzipMultipleFilesAsInputstreamToByteArray() throws IOException { + + final Resource resource = 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); + + Assert.assertNotNull(resultMessage); + + @SuppressWarnings("unchecked") + Map unzippedData = (Map) resultMessage.getPayload(); + + Assert.assertNotNull(unzippedData); + Assert.assertTrue(unzippedData.size() == 5); + + } + + /** + * UnCompress a ZIP archive containing multiple files. The result will be + * a collection of files. + * + * @throws IOException + */ + @Test + public void unzipMultipleFilesAsInputstreamWithExpectSingleResultTrue() throws IOException { + + final Resource resource = 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(); + + try { + unZipTransformer.transform(message); + } + catch (MessagingException e) { + Assert.assertTrue(e.getMessage().contains("The UnZip operation extracted " + + "5 result objects but expectSingleResult was 'true'.")); + return; + } + + Assert.fail("Expected a MessagingException to be thrown."); + + } + + @Test + public void unzipInvalidZipFile() throws FileNotFoundException, IOException, InterruptedException { + + final File fileToUnzip = testFolder.newFile(); + FileUtils.writeStringToFile(fileToUnzip, "hello world"); + + final UnZipTransformer unZipTransformer = new UnZipTransformer(); + unZipTransformer.setZipResultType(ZipResultType.BYTE_ARRAY); + unZipTransformer.setExpectSingleResult(true); + unZipTransformer.afterPropertiesSet(); + + final Message message = MessageBuilder.withPayload(fileToUnzip).build(); + + try { + unZipTransformer.transform(message); + } + catch (MessagingException e) { + Assert.assertTrue(e.getMessage().contains(String.format("Not a zip file: '%s'.", fileToUnzip.getAbsolutePath()))); + return; + } + + Assert.fail("Expected a MessagingException to be thrown."); + } +} diff --git a/spring-integration-zip/src/test/java/org/springframework/integration/zip/transformer/UnZipTransformerTests.xml b/spring-integration-zip/src/test/java/org/springframework/integration/zip/transformer/UnZipTransformerTests.xml new file mode 100644 index 0000000..aa47808 --- /dev/null +++ b/spring-integration-zip/src/test/java/org/springframework/integration/zip/transformer/UnZipTransformerTests.xml @@ -0,0 +1,10 @@ + + + + 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 0000000..678bf02 --- /dev/null +++ b/spring-integration-zip/src/test/java/org/springframework/integration/zip/transformer/ZipTransformerTests.java @@ -0,0 +1,284 @@ +/* + * Copyright 2015 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 + * + * http://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 static org.mockito.Mockito.mock; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +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.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.springframework.messaging.Message; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.integration.zip.ZipHeaders; +import org.zeroturnaround.zip.ZipUtil; + +/** + * + * @author Gunnar Hillert + * @since 1.0 + * + */ +public class ZipTransformerTests { + + @Rule + public TemporaryFolder testFolder = new TemporaryFolder(); + + /** + * Compress a simple String. The result will be a byte array. + * + * @throws FileNotFoundException + * @throws IOException + */ + @Test + public void zipString() throws FileNotFoundException, IOException { + 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(); + + Assert.assertTrue("Expected payload to be an instance of byte but was " + + resultPayload.getClass().getName(), resultPayload instanceof byte[]); + + final File temporaryTestDirectory = testFolder.newFolder(); + + ZipUtil.unpack(new ByteArrayInputStream((byte[]) resultPayload), temporaryTestDirectory); + + final File unzippedEntry = new File(temporaryTestDirectory, "test.txt"); + Assert.assertTrue(unzippedEntry.exists()); + Assert.assertTrue(unzippedEntry.isFile()); + + //See http://stackoverflow.com/questions/3725662/what-is-the-earliest-timestamp-value-that-is-supported-in-zip-file-format + Assert.assertTrue((fileDate.getTime() - 3000) < unzippedEntry.lastModified()); + Assert.assertTrue((fileDate.getTime() + 3000) > unzippedEntry.lastModified()); + } + + @Test + public void zipStringCollection() throws FileNotFoundException, IOException { + 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(); + + Assert.assertTrue("Expected payload to be an instance of byte but was " + + resultPayload.getClass().getName(), resultPayload instanceof byte[]); + + final File temporaryTestDirectory = testFolder.newFolder(); + + ZipUtil.unpack(new ByteArrayInputStream((byte[]) resultPayload), temporaryTestDirectory); + + File[] files = temporaryTestDirectory.listFiles(); + + Assert.assertTrue(files.length >= 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")) { + Assert.assertTrue(file.exists()); + Assert.assertTrue(file.isFile()); + + //See http://stackoverflow.com/questions/3725662/what-is-the-earliest-timestamp-value-that-is-supported-in-zip-file-format + Assert.assertTrue(String.format("%s : %s", fileDate.getTime() - 4000, file.lastModified()), (fileDate.getTime() - 4000) < file.lastModified()); + Assert.assertTrue((fileDate.getTime() + 4000) > file.lastModified()); + + Assert.assertTrue( + String.format("File '%s' did not end with '.txt'.", file.getName()), + file.getName().endsWith(".txt")); + + Assert.assertTrue(expectedFileNames.contains(file.getName())); + } + + } + + } + + @Test + public void zipStringToFile() throws FileNotFoundException, IOException { + final ZipTransformer zipTransformer = new ZipTransformer(); + zipTransformer.setBeanFactory(mock(BeanFactory.class)); + zipTransformer.setZipResultType(ZipResultType.FILE); + 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); + + Assert.assertTrue("Expected payload to be an instance of file but was " + + result.getPayload().getClass().getName(), result.getPayload() instanceof File); + + final File payload = (File) result.getPayload(); + + System.out.println(payload.getAbsolutePath()); + + Assert.assertEquals(message.getHeaders().getId().toString() + ".msg.zip", payload.getName()); + Assert.assertTrue(SpringZipUtils.isValid(payload)); + + final byte[] zipEntryData = ZipUtil.unpackEntry(payload, "test.txt"); + + Assert.assertNotNull("Entry '" + zipEntryFileName + "' was not found.", zipEntryData); + Assert.assertTrue("Hello World".equals(new String(zipEntryData))); + + } + + @Test + public void zipFile() throws IOException { + + final ZipTransformer zipTransformer = new ZipTransformer(); + zipTransformer.setBeanFactory(mock(BeanFactory.class)); + zipTransformer.afterPropertiesSet(); + + final File testFile = createTestFile(10); + + Assert.assertTrue(testFile.exists()); + + final Message message = MessageBuilder.withPayload(testFile).build(); + + final Message result = zipTransformer.transform(message); + + Assert.assertTrue(result.getPayload() instanceof File); + + final File payload = (File) result.getPayload(); + + Assert.assertEquals(testFile.getName() + ".zip", payload.getName()); + Assert.assertTrue(SpringZipUtils.isValid(payload)); + } + + @Test + public void zipCollection() throws IOException { + + final File testFile1 = createTestFile(1); + final File testFile2 = createTestFile(2); + final File testFile3 = createTestFile(3); + final File testFile4 = createTestFile(4); + + Assert.assertTrue(testFile1.exists()); + Assert.assertTrue(testFile2.exists()); + Assert.assertTrue(testFile3.exists()); + Assert.assertTrue(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); + + Assert.assertTrue(result.getPayload() instanceof File); + + final File outputZipFile = (File) result.getPayload(); + + Assert.assertTrue(outputZipFile.exists()); + Assert.assertTrue(outputZipFile.isFile()); + Assert.assertTrue(outputZipFile.getName().endsWith("zip")); + Assert.assertTrue(SpringZipUtils.isValid(outputZipFile)); + + } + + private File createTestFile(int size) throws IOException { + + final File temporaryTestDirectory = testFolder.newFolder(); + + final File testFile = new File(temporaryTestDirectory, "testdata" + UUID.randomUUID().toString() + ".data"); + + RandomAccessFile f = null; + try { + f = new RandomAccessFile(testFile, "rw"); + f.setLength(size * 1024 * 1024); + } + catch (Exception e) { + System.err.println(e); + } + finally { + try { + if (f != null) { + f.close(); + } + } catch (IOException e) {} + } + return testFile; + + } + +} diff --git a/spring-integration-zip/src/test/resources/log4j.properties b/spring-integration-zip/src/test/resources/log4j.properties new file mode 100644 index 0000000..1bcfd18 --- /dev/null +++ b/spring-integration-zip/src/test/resources/log4j.properties @@ -0,0 +1,8 @@ +log4j.rootCategory=WARN, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss.SSS} %-5p [%t][%c] %m%n + +log4j.category.org.springframework.integration=INFO +log4j.category.org.springframework.integration.zip=INFO 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 0000000000000000000000000000000000000000..89e7e4d9a93271dff3ba69e51ce496f77d32ac01 GIT binary patch literal 657 zcmWIWW@h1HVBp|jXl=df#N(W$&%waJzy`$yIeH}(CEO>@d!F%TWC(z(%>$}!Qs9b2 zR-0A?Qd_L;b;jF|6-jL>Q0?{|qEk76X0k)gO-Ti-_18V+duBBYlHy{5EuJvWIfn|4x&eZ;jLpUkckwK$QrQ)B*L-{s2UN$ifjb707V$_8r29` Y@CJCZf+7Hz*w`2%85tOqfS7>+06<=Vx&QzG literal 0 HcmV?d00001 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 0000000..a287965 --- /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 0000000..c6af3c9 --- /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 0000000..646bce2 --- /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 0000000..b9a7c52 --- /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 0000000..639acf6 --- /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/single.zip b/spring-integration-zip/src/test/resources/testzipdata/single.zip new file mode 100644 index 0000000000000000000000000000000000000000..9f61c70a8ca287827eae4a374a66619b03e3dc5f GIT binary patch literal 145 zcmWIWW@h1HVBp|jVA_AfDb~65hBN~MgCr0GMHz}S^U`xt^-3yAxb-wno$(I;a7NqH zSKmwL4mW{1b8zti7?|f1ZWNzENKK$2qOc$S=m4$j6i4sq@6(=1^^j# B9|-^e literal 0 HcmV?d00001