Migrate Zip Extension as a core module

This commit is contained in:
abilan
2023-02-16 13:06:44 -05:00
parent 39c73d404a
commit 210a2f9d2c
48 changed files with 2821 additions and 5 deletions

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2015-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.zip;
/**
* Zip adapter specific message headers.
*
* @author Gunnar Hillert
*
* @since 6.1
*/
public abstract class ZipHeaders {
public static final String PREFIX = "zip_";
public static final String ZIP_ENTRY_FILE_NAME = PREFIX + "entryFilename";
public static final String ZIP_ENTRY_PATH = PREFIX + "entryPath";
public static final String ZIP_ENTRY_LAST_MODIFIED_DATE = PREFIX + "entryLastModifiedDate";
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2015-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.zip.config.xml;
import org.w3c.dom.Element;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.xml.AbstractTransformerParser;
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
import org.springframework.util.StringUtils;
/**
* Base class for Zip transformer parsers.
*
* @author Gunnar Hillert
* @author Artem Bilan
*
* @since 6.1
*/
public abstract class AbstractZipTransformerParser extends AbstractTransformerParser {
/**
* @param element The XML Element to process
* @param parserContext The Spring ParserContext
* @param builder BeanDefinitionBuilder for constructing Bean Definitions
*/
@Override
protected final void parseTransformer(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
String deleteFiles = element.getAttribute("delete-files");
if (StringUtils.hasText(deleteFiles)) {
builder.addPropertyValue("deleteFiles", deleteFiles);
}
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "charset");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "result-type", "zipResultType");
postProcessTransformer(element, parserContext, builder);
}
/**
* Subclasses may override this method to provide additional configuration.
*
* @param element The XML Element to process
* @param parserContext The Spring ParserContext
* @param builder BeanDefinitionBuilder for constructing Bean Definitions
*/
protected void postProcessTransformer(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2015-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.zip.config.xml;
import org.w3c.dom.Element;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
import org.springframework.integration.zip.transformer.UnZipTransformer;
/**
* Parser for the 'unzip-transformer' element.
*
* @author Gunnar Hillert
* @author Artem Bilan
*
* @since 6.1
*/
public class UnZipTransformerParser extends AbstractZipTransformerParser {
@Override
protected String getTransformerClassName() {
return UnZipTransformer.class.getName();
}
@Override
protected void postProcessTransformer(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "expect-single-result");
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2015-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.zip.config.xml;
import org.springframework.integration.config.xml.AbstractIntegrationNamespaceHandler;
/**
* The namespace handler for the Zip namespace
*
* @author Gunnar Hillert
*
* @since 6.1
*
*/
public class ZipNamespaceHandler extends AbstractIntegrationNamespaceHandler {
@Override
public void init() {
this.registerBeanDefinitionParser("zip-transformer", new ZipTransformerParser());
this.registerBeanDefinitionParser("unzip-transformer", new UnZipTransformerParser());
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2015-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.zip.config.xml;
import org.w3c.dom.Element;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
import org.springframework.integration.zip.transformer.ZipTransformer;
/**
* Parser for the 'zip-transformer' element.
*
* @author Gunnar Hillert
* @author Artem Bilan
*
* @since 6.1
*/
public class ZipTransformerParser extends AbstractZipTransformerParser {
@Override
protected String getTransformerClassName() {
return ZipTransformer.class.getName();
}
@Override
protected void postProcessTransformer(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "compression-level");
}
}

View File

@@ -0,0 +1,4 @@
/**
* Provides parser classes to provide Xml namespace support for the Zip components.
*/
package org.springframework.integration.zip.config.xml;

View File

@@ -0,0 +1,4 @@
/**
* Root package of the Zip Module.
*/
package org.springframework.integration.zip;

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2015-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.zip.splitter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.commons.io.FilenameUtils;
import org.springframework.integration.file.FileHeaders;
import org.springframework.integration.splitter.AbstractMessageSplitter;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.zip.ZipHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.util.Assert;
/**
*
* @author Gunnar Hillert
* @author Andriy Kryvtsun
* @author Artem Bilan
*
* @since 6.1
*/
public class UnZipResultSplitter extends AbstractMessageSplitter {
@Override
@SuppressWarnings("unchecked")
protected Object splitMessage(Message<?> message) {
Assert.state(message.getPayload() instanceof Map,
"The UnZipResultSplitter supports only Map<String, Object> payload");
Map<String, Object> unzippedEntries = (Map<String, Object>) message.getPayload();
MessageHeaders headers = message.getHeaders();
List<MessageBuilder<Object>> messageBuilders = new ArrayList<>(unzippedEntries.size());
for (Map.Entry<String, Object> entry : unzippedEntries.entrySet()) {
String path = FilenameUtils.getPath(entry.getKey());
String filename = FilenameUtils.getName(entry.getKey());
MessageBuilder<Object> messageBuilder = MessageBuilder.withPayload(entry.getValue())
.setHeader(FileHeaders.FILENAME, filename)
.setHeader(ZipHeaders.ZIP_ENTRY_PATH, path)
.copyHeadersIfAbsent(headers);
messageBuilders.add(messageBuilder);
}
return messageBuilders;
}
}

View File

@@ -0,0 +1,4 @@
/**
* Classes to support Splitter pattern for Zip.
*/
package org.springframework.integration.zip.splitter;

View File

@@ -0,0 +1,117 @@
/*
* Copyright 2015-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.zip.transformer;
import java.io.File;
import java.nio.charset.Charset;
import org.springframework.integration.file.DefaultFileNameGenerator;
import org.springframework.integration.file.FileNameGenerator;
import org.springframework.integration.transformer.AbstractTransformer;
import org.springframework.messaging.Message;
import org.springframework.util.Assert;
/**
* Base class for transformers that provide Zip compression.
*
* @author Gunnar Hillert
* @author Artem Bilan
*
* @since 6.1
*/
public abstract class AbstractZipTransformer extends AbstractTransformer {
protected Charset charset = Charset.defaultCharset();
protected FileNameGenerator fileNameGenerator;
protected ZipResultType zipResultType = ZipResultType.FILE;
protected File workDirectory =
new File(System.getProperty("java.io.tmpdir") + File.separator + "ziptransformer");
protected boolean deleteFiles;
/**
* If the payload is an instance of {@link File}, this property specifies
* whether to delete the {@link File} after transformation.
* Default is <em>false</em>.
* @param deleteFiles Defaults to <em>false</em> if not set
*/
public void setDeleteFiles(boolean deleteFiles) {
this.deleteFiles = deleteFiles;
}
/**
* Set the work-directory. The work directory is used when the {@link ZipResultType}
* is set to {@link ZipResultType#FILE}. By default, this property is set to
* the System temporary directory containing a subdirectory "ziptransformer".
* @param workDirectory Must not be null and must not represent a file.
*/
public void setWorkDirectory(File workDirectory) {
Assert.notNull(workDirectory, "workDirectory must not be null.");
Assert.isTrue(workDirectory.isDirectory(), "The workDirectory specified must be a directory.");
this.workDirectory = workDirectory;
}
/**
* Define the format of the data returned after transformation. Available
* options are:
* <ul>
* <li>File</li>
* <li>Byte Array</li>
* </ul>
* Defaults to {@link ZipResultType#FILE}.
* @param zipResultType Must not be null
*/
public void setZipResultType(ZipResultType zipResultType) {
Assert.notNull(zipResultType, "The zipResultType must not be empty.");
this.zipResultType = zipResultType;
}
@Override
protected void onInit() {
super.onInit();
if (!this.workDirectory.exists()) {
logger.info(() -> "Creating work directory: " + this.workDirectory);
Assert.isTrue(this.workDirectory.mkdirs(), () -> "Can't create the 'workDirectory': " + this.workDirectory);
}
DefaultFileNameGenerator defaultFileNameGenerator = new DefaultFileNameGenerator();
defaultFileNameGenerator.setBeanFactory(getBeanFactory());
defaultFileNameGenerator.setConversionService(getConversionService());
this.fileNameGenerator = defaultFileNameGenerator;
}
/**
* @param message the message and its payload must not be null.
*/
@Override
protected Object doTransform(Message<?> message) {
return doZipTransform(message);
}
/**
* Subclasses must implement this method to provide the Zip transformation
* logic.
* @param message The message will never be null.
* @return The result of the Zip transformation.
*/
protected abstract Object doZipTransform(Message<?> message);
}

View File

@@ -0,0 +1,137 @@
/*
* Copyright 2015-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.zip.transformer;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import org.apache.commons.io.IOUtils;
import org.zeroturnaround.zip.ZipEntrySource;
import org.zeroturnaround.zip.ZipException;
import org.springframework.core.log.LogAccessor;
/**
* Once the Spring Integration Zip support matures, we need to contribute the
* methods in this utility class back to the ZT Zip project.
*
* @author Gunnar Hillert
*
* @since 6.1
*/
public class SpringZipUtils {
private static final LogAccessor logger = new LogAccessor(SpringZipUtils.class);
public static byte[] pack(Collection<ZipEntrySource> entries, int compressionLevel) {
logger.debug(() -> "Creating byte array from: " + entries);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
pack(entries, outputStream, compressionLevel);
return outputStream.toByteArray();
}
public static void pack(Collection<ZipEntrySource> entries, File zip, int compressionLevel) {
logger.debug(() -> "Creating '" + zip + "' from " + entries + ".");
FileOutputStream outputStream;
try {
outputStream = new FileOutputStream(zip);
}
catch (FileNotFoundException e) {
throw new IllegalStateException(String.format("File '%s' not found.", zip.getAbsolutePath()), e);
}
pack(entries, outputStream, compressionLevel);
}
private static void pack(Collection<ZipEntrySource> entries, OutputStream outputStream, int compressionLevel) {
ZipOutputStream out = null;
final BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream);
try {
out = new ZipOutputStream(bufferedOutputStream);
out.setLevel(compressionLevel);
for (ZipEntrySource entry : entries) {
addEntry(entry, out);
}
}
catch (IOException e) {
throw rethrow(e);
}
finally {
IOUtils.closeQuietly(out);
}
}
private static void addEntry(ZipEntrySource entry, ZipOutputStream out) throws IOException {
out.putNextEntry(entry.getEntry());
InputStream in = entry.getInputStream();
if (in != null) {
try {
IOUtils.copy(in, out);
}
finally {
IOUtils.closeQuietly(in);
}
}
out.closeEntry();
}
private static ZipException rethrow(IOException e) {
throw new ZipException(e);
}
public static void copy(InputStream in, File file) throws IOException {
OutputStream out = new BufferedOutputStream(new FileOutputStream(file));
try {
IOUtils.copy(in, out);
}
finally {
IOUtils.closeQuietly(out);
}
}
public byte[] copy(InputStream in) throws IOException {
return IOUtils.toByteArray(in);
}
static boolean isValid(final File file) {
ZipFile zipfile = null;
try {
zipfile = new ZipFile(file);
return true;
}
catch (IOException e) {
return false;
}
finally {
IOUtils.closeQuietly(zipfile);
}
}
}

View File

@@ -0,0 +1,205 @@
/*
* Copyright 2015-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.zip.transformer;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.zip.ZipEntry;
import org.apache.commons.io.IOUtils;
import org.zeroturnaround.zip.ZipEntryCallback;
import org.zeroturnaround.zip.ZipException;
import org.zeroturnaround.zip.ZipUtil;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.MessagingException;
/**
* Transformer implementation that applies an UnZip transformation to the message
* payload.
*
* @author Gunnar Hillert
* @author Artem Bilan
* @author Ingo Dueppe
*
* @since 6.1
*/
public class UnZipTransformer extends AbstractZipTransformer {
private volatile boolean expectSingleResult = false;
/**
*
* This parameter indicates that only one result object shall be returned as
* a result from the executed Unzip operation. If set to <code>true</code> 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 <code>true</code>, then a
* {@link MessagingException} is thrown.
* If set to <code>false</code>, the complete result list is returned as the
* payload. This is the {@code default}.
* @param expectSingleResult If not set explicitly, will default to false
*/
public void setExpectSingleResult(boolean expectSingleResult) {
this.expectSingleResult = expectSingleResult;
}
@Override
protected Object doZipTransform(final Message<?> message) {
try {
Object payload = message.getPayload();
Object unzippedData;
InputStream inputStream = null;
try {
if (payload instanceof final File filePayload) {
if (filePayload.isDirectory()) {
throw new UnsupportedOperationException("Cannot unzip a directory: " +
filePayload.getAbsolutePath());
}
if (!SpringZipUtils.isValid(filePayload)) {
throw new IllegalStateException("Not a zip file: " + filePayload.getAbsolutePath());
}
inputStream = new FileInputStream(filePayload);
}
else if (payload instanceof InputStream) {
inputStream = (InputStream) payload;
}
else if (payload instanceof byte[]) {
inputStream = new ByteArrayInputStream((byte[]) payload);
}
else {
throw new IllegalArgumentException("Unsupported payload type '" + payload.getClass().getSimpleName()
+ "'. The only supported payload types are java.io.File, byte[] and java.io.InputStream");
}
final SortedMap<String, Object> uncompressedData = new TreeMap<>();
ZipUtil.iterate(inputStream, new ZipEntryCallback() {
@Override
public void process(InputStream zipEntryInputStream, ZipEntry zipEntry) throws IOException {
final String zipEntryName = zipEntry.getName();
final long zipEntryTime = zipEntry.getTime();
final long zipEntryCompressedSize = zipEntry.getCompressedSize();
final String type = zipEntry.isDirectory() ? "directory" : "file";
logger.info(() -> String.format("Unpacking Zip Entry - Name: '%s',Time: '%s', " +
"Compressed Size: '%s', Type: '%s'",
zipEntryName, zipEntryTime, zipEntryCompressedSize, type));
if (ZipResultType.FILE.equals(zipResultType)) {
final File destinationFile = checkPath(message, zipEntryName);
if (zipEntry.isDirectory()) {
destinationFile.mkdirs(); //NOSONAR false positive
}
else {
mkDirOfAncestorDirectories(destinationFile);
SpringZipUtils.copy(zipEntryInputStream, destinationFile);
uncompressedData.put(zipEntryName, destinationFile);
}
}
else if (ZipResultType.BYTE_ARRAY.equals(zipResultType)) {
if (!zipEntry.isDirectory()) {
checkPath(message, zipEntryName);
byte[] data = IOUtils.toByteArray(zipEntryInputStream);
uncompressedData.put(zipEntryName, data);
}
}
else {
throw new IllegalStateException("Unsupported zipResultType: " + zipResultType);
}
}
public File checkPath(final Message<?> message, final String zipEntryName) throws IOException {
final File tempDir = new File(workDirectory, message.getHeaders().getId().toString());
tempDir.mkdirs(); //NOSONAR false positive
final File destinationFile = new File(tempDir, zipEntryName);
/* If we see the relative traversal string of ".." we need to make sure
* that the outputdir + name doesn't leave the outputdir.
*/
if (!destinationFile.getCanonicalPath()
.startsWith(tempDir.getCanonicalPath() + File.separator)) {
throw new ZipException("The file " + zipEntryName +
" is trying to leave the target output directory of " + workDirectory);
}
return destinationFile;
}
});
if (uncompressedData.isEmpty()) {
logger.warn(() -> "No data unzipped from payload with message Id " + message.getHeaders().getId());
unzippedData = null;
}
else {
if (this.expectSingleResult) {
if (uncompressedData.size() == 1) {
unzippedData = uncompressedData.values().iterator().next();
}
else {
throw new MessagingException(message,
String.format("The UnZip operation extracted %s "
+ "result objects but expectSingleResult was 'true'.", uncompressedData
.size()));
}
}
else {
unzippedData = uncompressedData;
}
}
IOUtils.closeQuietly(inputStream);
if (payload instanceof final File filePayload && this.deleteFiles) {
if (!filePayload.delete() && logger.isWarnEnabled()) {
logger.warn(() -> "failed to delete File '" + filePayload + "'");
}
}
}
finally {
IOUtils.closeQuietly(inputStream);
}
return unzippedData;
}
catch (Exception e) {
throw new MessageHandlingException(message, "Failed to apply Zip transformation.", e);
}
}
private static void mkDirOfAncestorDirectories(File destinationFile) {
File parentDirectory = destinationFile.getParentFile();
if (parentDirectory != null) {
parentDirectory.mkdirs();
}
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2015-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.zip.transformer;
/**
* @author Gunnar Hillert
*
* @since 6.1
*/
public enum ZipResultType {
FILE, BYTE_ARRAY
}

View File

@@ -0,0 +1,223 @@
/*
* Copyright 2015-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.zip.transformer;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.zip.Deflater;
import org.apache.commons.io.FilenameUtils;
import org.zeroturnaround.zip.ByteSource;
import org.zeroturnaround.zip.FileSource;
import org.zeroturnaround.zip.ZipEntrySource;
import org.springframework.integration.file.FileHeaders;
import org.springframework.integration.transformer.Transformer;
import org.springframework.integration.zip.ZipHeaders;
import org.springframework.messaging.Message;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
/**
* {@link Transformer} implementation that applies a Zip transformation to the
* message payload. Keep in mind that Zip entry timestamps are recorded only to
* two 2 second precision:
* <p>
* See also: <a href="http://mindprod.com/jgloss/zip.html"/>
* <p>
* If you want to generate Zip files larger than {@code 4GB}, you must use Java 7:
* <p>
* See also: <a href="https://blogs.oracle.com/xuemingshen/entry/zip64_support_for_4g_zipfile"/>
*
* @author Gunnar Hillert
* @author Artem Bilan
*
* @since 6.1
*/
public class ZipTransformer extends AbstractZipTransformer {
private static final String ZIP_EXTENSION = ".zip";
private volatile int compressionLevel = Deflater.DEFAULT_COMPRESSION;
private volatile boolean useFileAttributes = true;
/**
* Set the compression level. Default is {@link Deflater#DEFAULT_COMPRESSION}.
* @param compressionLevel Must be an integer value from 0-9.
*/
public void setCompressionLevel(int compressionLevel) {
Assert.isTrue(compressionLevel >= 0 && compressionLevel <= 9, "Acceptable levels are 0-9");
this.compressionLevel = compressionLevel;
}
/**
* Specify whether the name of the file shall be used for the zip entry.
* @param useFileAttributes Defaults to true if not set explicitly
*/
public void setUseFileAttributes(boolean useFileAttributes) {
this.useFileAttributes = useFileAttributes;
}
/**
* The payload may encompass the following types:
* <ul>
* <li>{@link File}
*...<li>{@link String}
*...<li>byte[]
*...<li>{@link Iterable}
* </ul>
* When providing an {@link Iterable}, nested Iterables are not supported. However,
* payloads can be any of the other supported types.
*/
@Override
protected Object doZipTransform(Message<?> message) {
final Object payload = message.getPayload();
final Object zippedData;
final String baseFileName = this.fileNameGenerator.generateFileName(message);
final String zipEntryName;
final String zipFileName;
if (message.getHeaders().containsKey(ZipHeaders.ZIP_ENTRY_FILE_NAME)) {
zipEntryName = (String) message.getHeaders().get(ZipHeaders.ZIP_ENTRY_FILE_NAME);
}
else {
zipEntryName = baseFileName;
}
if (message.getHeaders().containsKey(FileHeaders.FILENAME)) {
zipFileName = (String) message.getHeaders().get(FileHeaders.FILENAME);
}
else {
zipFileName = baseFileName + ZIP_EXTENSION;
}
final Date lastModifiedDate;
if (message.getHeaders().containsKey(ZipHeaders.ZIP_ENTRY_LAST_MODIFIED_DATE)) {
lastModifiedDate = (Date) message.getHeaders().get(ZipHeaders.ZIP_ENTRY_LAST_MODIFIED_DATE);
}
else {
lastModifiedDate = new Date();
}
java.util.List<ZipEntrySource> entries = new ArrayList<>();
if (payload instanceof Iterable<?>) {
int counter = 1;
String baseName = FilenameUtils.getBaseName(zipEntryName);
String fileExtension = FilenameUtils.getExtension(zipEntryName);
if (StringUtils.hasText(fileExtension)) {
fileExtension = FilenameUtils.EXTENSION_SEPARATOR_STR + fileExtension;
}
for (Object item : (Iterable<?>) payload) {
final ZipEntrySource zipEntrySource = createZipEntrySource(item, lastModifiedDate, baseName + "_"
+ counter + fileExtension, this.useFileAttributes);
logger.debug(() -> "ZipEntrySource path: '" + zipEntrySource.getPath() + "'");
entries.add(zipEntrySource);
counter++;
}
}
else {
final ZipEntrySource zipEntrySource =
createZipEntrySource(payload, lastModifiedDate, zipEntryName, this.useFileAttributes);
entries.add(zipEntrySource);
}
final byte[] zippedBytes = SpringZipUtils.pack(entries, this.compressionLevel);
if (ZipResultType.FILE.equals(this.zipResultType)) {
final File zippedFile = new File(this.workDirectory, zipFileName);
try {
FileCopyUtils.copy(zippedBytes, zippedFile);
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
zippedData = zippedFile;
}
else if (ZipResultType.BYTE_ARRAY.equals(this.zipResultType)) {
zippedData = zippedBytes;
}
else {
throw new IllegalStateException("Unsupported zipResultType " + this.zipResultType);
}
if (this.deleteFiles) {
if (payload instanceof Iterable<?>) {
for (Object item : (Iterable<?>) payload) {
deleteFile(item);
}
}
else {
deleteFile(payload);
}
}
return getMessageBuilderFactory()
.withPayload(zippedData)
.copyHeaders(message.getHeaders())
.setHeader(FileHeaders.FILENAME, zipFileName)
.build();
}
private void deleteFile(Object fileToDelete) {
if (fileToDelete instanceof File && !((File) fileToDelete).delete()) {
logger.warn(() -> "Failed to delete File '" + fileToDelete + "'");
}
}
private ZipEntrySource createZipEntrySource(Object item,
Date lastModifiedDate, String zipEntryName, boolean useFileAttributes) {
if (item instanceof final File filePayload) {
String fileName = useFileAttributes ? filePayload.getName() : zipEntryName;
if (((File) item).isDirectory()) {
throw new UnsupportedOperationException("Zipping of directories is not supported.");
}
return new FileSource(fileName, filePayload);
}
else if (item instanceof byte[] || item instanceof String) {
byte[] bytesToCompress;
if (item instanceof String) {
bytesToCompress = ((String) item).getBytes(this.charset);
}
else {
bytesToCompress = (byte[]) item;
}
return new ByteSource(zipEntryName, bytesToCompress, lastModifiedDate.getTime());
}
else {
throw new IllegalArgumentException("Unsupported payload type. The only supported payloads are " +
"java.io.File, java.lang.String, and byte[]");
}
}
}

View File

@@ -0,0 +1,4 @@
/**
* Classes to support Transformer pattern for Zip.
*/
package org.springframework.integration.zip.transformer;

View File

@@ -0,0 +1 @@
http\://www.springframework.org/schema/integration/zip=org.springframework.integration.zip.config.xml.ZipNamespaceHandler

View File

@@ -0,0 +1,4 @@
http\://www.springframework.org/schema/integration/zip/spring-integration-zip-1.0.xsd=org/springframework/integration/zip/config/spring-integration-zip.xsd
http\://www.springframework.org/schema/integration/zip/spring-integration-zip.xsd=org/springframework/integration/zip/config/spring-integration-zip.xsd
https\://www.springframework.org/schema/integration/zip/spring-integration-zip-1.0.xsd=org/springframework/integration/zip/config/spring-integration-zip.xsd
https\://www.springframework.org/schema/integration/zip/spring-integration-zip.xsd=org/springframework/integration/zip/config/spring-integration-zip.xsd

View File

@@ -0,0 +1,4 @@
# Tooling related information for the integration Zip namespace
http\://www.springframework.org/schema/integration/zip@name=integration Zip Namespace
http\://www.springframework.org/schema/integration/zip@prefix=int-zip
http\://www.springframework.org/schema/integration/zip@icon=org/springframework/integration/zip/config/spring-integration-zip.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 B

View File

@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.springframework.org/schema/integration/zip"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:integration="http://www.springframework.org/schema/integration"
targetNamespace="http://www.springframework.org/schema/integration/zip"
elementFormDefault="qualified">
<xsd:import namespace="http://www.springframework.org/schema/beans" />
<xsd:import namespace="http://www.springframework.org/schema/tool" />
<xsd:import namespace="http://www.springframework.org/schema/integration"
schemaLocation="https://www.springframework.org/schema/integration/spring-integration.xsd" />
<xsd:annotation>
<xsd:documentation>
Defines the configuration elements for the Spring Integration
Zip Adapter.
</xsd:documentation>
</xsd:annotation>
<xsd:element name="zip-transformer">
<xsd:annotation>
<xsd:documentation>
Creates a Transformer that compresses message
payloads using Zip compressions. The following payload types are
supported:
- java.io.File
- byte[]
- String
</xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="transformerType">
<xsd:attribute name="compression-level" type="xsd:integer"
use="optional">
<xsd:annotation>
<xsd:documentation>
Sets the compression level. Default is
java.util.zip.Deflater.DEFAULT_COMPRESSION
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
<xsd:element name="unzip-transformer">
<xsd:annotation>
<xsd:documentation>
Creates a Transformer that decompresses message
payloads using Zip compressions. The following payload types are
supported:
- java.io.File
- byte[]
</xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="transformerType">
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
<xsd:complexType name="transformerType">
<xsd:sequence>
<xsd:element ref="integration:poller" minOccurs="0" maxOccurs="1"/>
<xsd:element name="request-handler-advice-chain" type="integration:handlerAdviceChainType" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="id" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
Identifies the underlying Spring bean definition (EventDrivenConsumer)
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attributeGroup ref="integration:inputOutputChannelGroup"/>
<xsd:attribute name="delete-files" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
If the payload is an instance of {@link File}, this
attribute specifies whether to delete the {@link File} after
transformation. The default is 'false'.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="expect-single-result" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
If set to 'true', this property indicates
that only one result object shall be returned as a result
from the executed Unzip operation. Defaults to 'false'.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="result-type">
<xsd:annotation>
<xsd:documentation>
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.
</xsd:documentation>
</xsd:annotation>
<xsd:simpleType>
<xsd:union memberTypes="resultType xsd:string" />
</xsd:simpleType>
</xsd:attribute>
</xsd:complexType>
<xsd:simpleType name="resultType">
<xsd:restriction base="xsd:token">
<xsd:enumeration value="BYTE_ARRAY" />
<xsd:enumeration value="FILE" />
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>