From 6d5690fd58a0dff89ba0349e2324bd98f82d8a39 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 31 Mar 2020 16:09:07 -0400 Subject: [PATCH] GH-2996: Add Resource for remote file transfer (#3231) * GH-2996: Add Resource for remote file transfer Fixes https://github.com/spring-projects/spring-integration/issues/2996 * Fix `RemoteFileTemplate.send()` to support a `Resource` payload for remote file transferring content * Code style clean up for `RemoteFileTemplate` * Remove `volatile` for configuration properties for better performance * Change a `charset` to the `Charset` for only once conversion from string during configuration phase * Fix (S)FTP tests for new functionality * Change affected tests to JUnit 5 * Document a new feature; mention all the supported types and `FileExistsMode` constants * * Fix language in `whats-new.adoc` --- .../file/remote/RemoteFileTemplate.java | 61 +++++---- .../file/remote/RemoteFileTemplateTests.java | 26 ++-- .../FtpOutboundChannelAdapterParserTests.java | 31 +++-- ...FtpsOutboundChannelAdapterParserTests.java | 28 ++-- .../OutboundChannelAdapterParserTests.java | 121 +++++++++--------- src/reference/asciidoc/ftp.adoc | 8 +- src/reference/asciidoc/sftp.adoc | 12 +- src/reference/asciidoc/whats-new.adoc | 6 + 8 files changed, 171 insertions(+), 122 deletions(-) diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/RemoteFileTemplate.java b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/RemoteFileTemplate.java index e0e7e71d27..8053166db4 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/RemoteFileTemplate.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/RemoteFileTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2019 the original author or authors. + * Copyright 2013-2020 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. @@ -23,6 +23,8 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.logging.Log; @@ -32,6 +34,7 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.Resource; import org.springframework.expression.Expression; import org.springframework.integration.file.DefaultFileNameGenerator; import org.springframework.integration.file.FileNameGenerator; @@ -65,7 +68,7 @@ public class RemoteFileTemplate implements RemoteFileOperations, Initializ private final Log logger = LogFactory.getLog(this.getClass()); /** - * the {@link SessionFactory} for acquiring remote file Sessions. + * The {@link SessionFactory} for acquiring remote file Sessions. */ protected final SessionFactory sessionFactory; // NOSONAR @@ -76,29 +79,29 @@ public class RemoteFileTemplate implements RemoteFileOperations, Initializ private final AtomicInteger activeTemplateCallbacks = new AtomicInteger(); - private volatile String temporaryFileSuffix = ".writing"; + private String temporaryFileSuffix = ".writing"; - private volatile boolean autoCreateDirectory = false; + private boolean autoCreateDirectory = false; - private volatile boolean useTemporaryFileName = true; + private boolean useTemporaryFileName = true; - private volatile ExpressionEvaluatingMessageProcessor directoryExpressionProcessor; + private ExpressionEvaluatingMessageProcessor directoryExpressionProcessor; - private volatile ExpressionEvaluatingMessageProcessor temporaryDirectoryExpressionProcessor; + private ExpressionEvaluatingMessageProcessor temporaryDirectoryExpressionProcessor; - private volatile ExpressionEvaluatingMessageProcessor fileNameProcessor; + private ExpressionEvaluatingMessageProcessor fileNameProcessor; - private volatile FileNameGenerator fileNameGenerator = new DefaultFileNameGenerator(); + private FileNameGenerator fileNameGenerator = new DefaultFileNameGenerator(); - private volatile boolean fileNameGeneratorSet; + private boolean fileNameGeneratorSet; - private volatile String charset = "UTF-8"; + private Charset charset = StandardCharsets.UTF_8; - private volatile String remoteFileSeparator = "/"; + private String remoteFileSeparator = "/"; - private volatile boolean hasExplicitlySetSuffix; + private boolean hasExplicitlySetSuffix; - private volatile BeanFactory beanFactory; + private BeanFactory beanFactory; /** * Construct a {@link RemoteFileTemplate} with the supplied session factory. @@ -216,7 +219,7 @@ public class RemoteFileTemplate implements RemoteFileOperations, Initializ * @param charset the charset. */ public void setCharset(String charset) { - this.charset = charset; + this.charset = Charset.forName(charset); } /** @@ -264,12 +267,12 @@ public class RemoteFileTemplate implements RemoteFileOperations, Initializ } @Override - public String append(final Message message) { + public String append(Message message) { return append(message, null); } @Override - public String append(final Message message, String subDirectory) { + public String append(Message message, String subDirectory) { return send(message, subDirectory, FileExistsMode.APPEND); } @@ -279,14 +282,14 @@ public class RemoteFileTemplate implements RemoteFileOperations, Initializ } @Override - public String send(final Message message, String subDirectory, FileExistsMode... mode) { + public String send(Message message, String subDirectory, FileExistsMode... mode) { FileExistsMode modeToUse = mode == null || mode.length < 1 || mode[0] == null ? FileExistsMode.REPLACE : mode[0]; return send(message, subDirectory, modeToUse); } - private String send(final Message message, final String subDirectory, final FileExistsMode mode) { + private String send(Message message, String subDirectory, FileExistsMode mode) { Assert.notNull(this.directoryExpressionProcessor, "'remoteDirectoryExpression' is required"); Assert.isTrue(!FileExistsMode.APPEND.equals(mode) || !this.useTemporaryFileName, "Cannot append when using a temporary file name"); @@ -354,17 +357,17 @@ public class RemoteFileTemplate implements RemoteFileOperations, Initializ } @Override - public boolean exists(final String path) { + public boolean exists(String path) { return execute(session -> session.exists(path)); } @Override - public boolean remove(final String path) { + public boolean remove(String path) { return execute(session -> session.remove(path)); } @Override - public void rename(final String fromPath, final String toPath) { + public void rename(String fromPath, String toPath) { Assert.hasText(fromPath, "Old filename cannot be null or empty"); Assert.hasText(toPath, "New filename cannot be null or empty"); @@ -390,7 +393,7 @@ public class RemoteFileTemplate implements RemoteFileOperations, Initializ } @Override - public boolean get(final String remotePath, final InputStreamCallback callback) { + public boolean get(String remotePath, InputStreamCallback callback) { Assert.notNull(remotePath, "'remotePath' cannot be null"); return execute(session -> { try (InputStream inputStream = session.readRaw(remotePath)) { @@ -419,7 +422,6 @@ public class RemoteFileTemplate implements RemoteFileOperations, Initializ return this.sessionFactory.getSession(); } - @SuppressWarnings("rawtypes") @Override public T execute(SessionCallback callback) { Session session = null; @@ -440,7 +442,7 @@ public class RemoteFileTemplate implements RemoteFileOperations, Initializ if (session != null) { session.dirty(); } - if (e instanceof MessagingException) { + if (e instanceof MessagingException) { // NOSONAR throw (MessagingException) e; } throw new MessagingException("Failed to execute on session", e); @@ -498,7 +500,7 @@ public class RemoteFileTemplate implements RemoteFileOperations, Initializ } } else if (payload instanceof byte[] || payload instanceof String) { - byte[] bytes = null; + byte[] bytes; if (payload instanceof String) { bytes = ((String) payload).getBytes(this.charset); name = "String payload"; @@ -513,6 +515,12 @@ public class RemoteFileTemplate implements RemoteFileOperations, Initializ dataInputStream = (InputStream) payload; name = "InputStream payload"; } + else if (payload instanceof Resource) { + Resource resource = (Resource) payload; + dataInputStream = resource.getInputStream(); + String filename = resource.getFilename(); + name = filename != null ? filename : "Resource payload"; + } else { throw new IllegalArgumentException("Unsupported payload type [" + payload.getClass().getName() @@ -563,6 +571,7 @@ public class RemoteFileTemplate implements RemoteFileOperations, Initializ private void doSend(Session session, FileExistsMode mode, String remoteFilePath, String tempFilePath, InputStream stream) throws IOException { + boolean rename = this.useTemporaryFileName; if (FileExistsMode.REPLACE.equals(mode)) { session.write(stream, tempFilePath); diff --git a/spring-integration-file/src/test/java/org/springframework/integration/file/remote/RemoteFileTemplateTests.java b/spring-integration-file/src/test/java/org/springframework/integration/file/remote/RemoteFileTemplateTests.java index 8a6d9b8ff2..f04520aa46 100644 --- a/spring-integration-file/src/test/java/org/springframework/integration/file/remote/RemoteFileTemplateTests.java +++ b/spring-integration-file/src/test/java/org/springframework/integration/file/remote/RemoteFileTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2020 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. @@ -32,14 +32,16 @@ import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.UUID; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.springframework.beans.factory.BeanFactory; +import org.springframework.core.io.ByteArrayResource; import org.springframework.expression.common.LiteralExpression; import org.springframework.integration.file.remote.session.Session; import org.springframework.integration.file.remote.session.SessionFactory; @@ -61,13 +63,13 @@ public class RemoteFileTemplateTests { private Session session; - @Rule - public TemporaryFolder folder = new TemporaryFolder(); + @TempDir + Path folder; private File file; @SuppressWarnings("unchecked") - @Before + @BeforeEach public void setUp() throws Exception { SessionFactory sessionFactory = mock(SessionFactory.class); this.template = new RemoteFileTemplate<>(sessionFactory); @@ -76,7 +78,7 @@ public class RemoteFileTemplateTests { this.template.afterPropertiesSet(); this.session = mock(Session.class); when(sessionFactory.getSession()).thenReturn(this.session); - this.file = this.folder.newFile(); + this.file = Files.createTempFile(this.folder, null, null).toFile(); } @Test @@ -146,6 +148,12 @@ public class RemoteFileTemplateTests { verify(this.session).write(any(InputStream.class), any()); } + @Test + public void testResource() throws IOException { + this.template.send(new GenericMessage<>(new ByteArrayResource("foo".getBytes())), FileExistsMode.IGNORE); + verify(this.session).write(any(InputStream.class), any()); + } + @Test public void testMissingFile() { this.template.send(new GenericMessage<>(new File(UUID.randomUUID().toString())), FileExistsMode.IGNORE); diff --git a/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/config/FtpOutboundChannelAdapterParserTests.java b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/config/FtpOutboundChannelAdapterParserTests.java index 4461c54c5c..9f63a49def 100644 --- a/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/config/FtpOutboundChannelAdapterParserTests.java +++ b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/config/FtpOutboundChannelAdapterParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -17,12 +17,13 @@ package org.springframework.integration.ftp.config; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import java.nio.charset.StandardCharsets; import java.util.Iterator; import java.util.Set; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.annotation.Autowired; @@ -45,18 +46,17 @@ import org.springframework.messaging.Message; import org.springframework.messaging.MessageHandler; import org.springframework.messaging.support.GenericMessage; import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; /** * @author Oleg Zhurakousky * @author Gary Russell * @author Gunnar Hillert * @author Artem Bilan + * * @since 2.0 */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig @DirtiesContext public class FtpOutboundChannelAdapterParserTests { @@ -87,7 +87,7 @@ public class FtpOutboundChannelAdapterParserTests { private FileNameGenerator fileNameGenerator; @Test - public void testFtpOutboundChannelAdapterComplete() throws Exception { + public void testFtpOutboundChannelAdapterComplete() { assertThat(TestUtils.getPropertyValue(ftpOutbound, "inputChannel")).isEqualTo(ftpChannel); assertThat(ftpOutbound.getComponentName()).isEqualTo("ftpOutbound"); FileTransferringMessageHandler handler = @@ -100,7 +100,7 @@ public class FtpOutboundChannelAdapterParserTests { assertThat(remoteFileSeparator).isEqualTo(""); assertThat(TestUtils.getPropertyValue(handler, "remoteFileTemplate.fileNameGenerator")) .isEqualTo(this.fileNameGenerator); - assertThat(TestUtils.getPropertyValue(handler, "remoteFileTemplate.charset")).isEqualTo("UTF-8"); + assertThat(TestUtils.getPropertyValue(handler, "remoteFileTemplate.charset")).isEqualTo(StandardCharsets.UTF_8); assertThat(TestUtils.getPropertyValue(handler, "remoteFileTemplate.directoryExpressionProcessor")).isNotNull(); assertThat(TestUtils.getPropertyValue(handler, "remoteFileTemplate.temporaryDirectoryExpressionProcessor")) .isNotNull(); @@ -122,9 +122,12 @@ public class FtpOutboundChannelAdapterParserTests { assertThat(TestUtils.getPropertyValue(ftpOutbound, "handler.mode")).isEqualTo(FileExistsMode.APPEND); } - @Test(expected = BeanCreationException.class) - public void testFailWithEmptyRfsAndAcdTrue() throws Exception { - new ClassPathXmlApplicationContext("FtpOutboundChannelAdapterParserTests-fail.xml", this.getClass()).close(); + @Test + public void testFailWithEmptyRfsAndAcdTrue() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> + new ClassPathXmlApplicationContext("FtpOutboundChannelAdapterParserTests-fail.xml", + getClass())); } @Test @@ -139,7 +142,7 @@ public class FtpOutboundChannelAdapterParserTests { @Test public void adviceChain() { MessageHandler handler = TestUtils.getPropertyValue(advisedAdapter, "handler", MessageHandler.class); - handler.handleMessage(new GenericMessage("foo")); + handler.handleMessage(new GenericMessage<>("foo")); assertThat(adviceCalled).isEqualTo(1); } @@ -152,7 +155,7 @@ public class FtpOutboundChannelAdapterParserTests { } @Test - public void testBeanExpressions() throws Exception { + public void testBeanExpressions() { FileTransferringMessageHandler handler = TestUtils.getPropertyValue(withBeanExpressions, "handler", FileTransferringMessageHandler.class); ExpressionEvaluatingMessageProcessor dirExpProc = TestUtils.getPropertyValue(handler, diff --git a/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/config/FtpsOutboundChannelAdapterParserTests.java b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/config/FtpsOutboundChannelAdapterParserTests.java index 875de089a0..9786d268e6 100644 --- a/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/config/FtpsOutboundChannelAdapterParserTests.java +++ b/spring-integration-ftp/src/test/java/org/springframework/integration/ftp/config/FtpsOutboundChannelAdapterParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -18,8 +18,9 @@ package org.springframework.integration.ftp.config; import static org.assertj.core.api.Assertions.assertThat; -import org.junit.Test; -import org.junit.runner.RunWith; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.integration.endpoint.EventDrivenConsumer; @@ -29,17 +30,17 @@ import org.springframework.integration.ftp.session.DefaultFtpsSessionFactory; import org.springframework.integration.test.util.TestUtils; import org.springframework.messaging.MessageChannel; import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; /** * @author Oleg Zhurakousky * @author Gunnar Hillert * @author Gary Russell + * @author Artem Bilan + * * @since 2.0 */ -@ContextConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +@SpringJUnitConfig @DirtiesContext public class FtpsOutboundChannelAdapterParserTests { @@ -53,15 +54,18 @@ public class FtpsOutboundChannelAdapterParserTests { private FileNameGenerator fileNameGenerator; @Test - public void testFtpsOutboundChannelAdapterComplete() throws Exception { - assertThat(ftpOutbound instanceof EventDrivenConsumer).isTrue(); + public void testFtpsOutboundChannelAdapterComplete() { + assertThat(ftpOutbound).isInstanceOf(EventDrivenConsumer.class); assertThat(TestUtils.getPropertyValue(ftpOutbound, "inputChannel")).isEqualTo(this.ftpChannel); assertThat(ftpOutbound.getComponentName()).isEqualTo("ftpOutbound"); - FileTransferringMessageHandler handler = TestUtils.getPropertyValue(ftpOutbound, "handler", FileTransferringMessageHandler.class); + FileTransferringMessageHandler handler = + TestUtils.getPropertyValue(ftpOutbound, "handler", FileTransferringMessageHandler.class); assertThat(TestUtils.getPropertyValue(handler, "remoteFileTemplate.fileNameGenerator")) .isEqualTo(this.fileNameGenerator); - assertThat(TestUtils.getPropertyValue(handler, "remoteFileTemplate.charset")).isEqualTo("UTF-8"); - DefaultFtpsSessionFactory sf = TestUtils.getPropertyValue(handler, "remoteFileTemplate.sessionFactory", DefaultFtpsSessionFactory.class); + assertThat(TestUtils.getPropertyValue(handler, "remoteFileTemplate.charset")).isEqualTo(StandardCharsets.UTF_8); + DefaultFtpsSessionFactory sf = + TestUtils.getPropertyValue(handler, "remoteFileTemplate.sessionFactory", + DefaultFtpsSessionFactory.class); assertThat(TestUtils.getPropertyValue(sf, "host")).isEqualTo("localhost"); assertThat(TestUtils.getPropertyValue(sf, "port")).isEqualTo(22); } diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/OutboundChannelAdapterParserTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/OutboundChannelAdapterParserTests.java index ad475f7e8f..9f397db705 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/OutboundChannelAdapterParserTests.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/OutboundChannelAdapterParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -17,15 +17,17 @@ package org.springframework.integration.sftp.config; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import java.nio.charset.StandardCharsets; import java.util.Iterator; import java.util.Set; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanDefinitionStoreException; -import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.expression.Expression; import org.springframework.expression.common.LiteralExpression; @@ -41,47 +43,54 @@ import org.springframework.integration.test.util.TestUtils; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHandler; import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; /** * @author Oleg Zhurakousky * @author Gary Russell * @author David Turanski * @author Gunnar Hillert + * @author Artem Bilan */ +@SpringJUnitConfig +@DirtiesContext public class OutboundChannelAdapterParserTests { private static volatile int adviceCalled; + @Autowired + ApplicationContext context; + @Test public void testOutboundChannelAdapterWithId() { - ConfigurableApplicationContext context = - new ClassPathXmlApplicationContext("OutboundChannelAdapterParserTests-context.xml", this.getClass()); - Object consumer = context.getBean("sftpOutboundAdapter"); - assertThat(consumer instanceof EventDrivenConsumer).isTrue(); - PublishSubscribeChannel channel = context.getBean("inputChannel", PublishSubscribeChannel.class); + EventDrivenConsumer consumer = this.context.getBean("sftpOutboundAdapter", EventDrivenConsumer.class); + PublishSubscribeChannel channel = this.context.getBean("inputChannel", PublishSubscribeChannel.class); assertThat(TestUtils.getPropertyValue(consumer, "inputChannel")).isEqualTo(channel); - assertThat(((EventDrivenConsumer) consumer).getComponentName()).isEqualTo("sftpOutboundAdapter"); + assertThat(consumer.getComponentName()).isEqualTo("sftpOutboundAdapter"); FileTransferringMessageHandler handler = TestUtils.getPropertyValue(consumer, "handler", FileTransferringMessageHandler.class); - String remoteFileSeparator = (String) TestUtils.getPropertyValue(handler, - "remoteFileTemplate.remoteFileSeparator"); + String remoteFileSeparator = + TestUtils.getPropertyValue(handler, "remoteFileTemplate.remoteFileSeparator", String.class); assertThat(remoteFileSeparator).isNotNull(); assertThat(remoteFileSeparator).isEqualTo("."); assertThat(TestUtils.getPropertyValue(handler, "remoteFileTemplate.temporaryFileSuffix", String.class)) .isEqualTo(".bar"); - Expression remoteDirectoryExpression = (Expression) TestUtils.getPropertyValue(handler, - "remoteFileTemplate.directoryExpressionProcessor.expression"); - assertThat(remoteDirectoryExpression).isNotNull(); - assertThat(remoteDirectoryExpression instanceof LiteralExpression).isTrue(); + Expression remoteDirectoryExpression = + TestUtils.getPropertyValue(handler, "remoteFileTemplate.directoryExpressionProcessor.expression", + Expression.class); + assertThat(remoteDirectoryExpression) + .isNotNull() + .isInstanceOf(LiteralExpression.class); assertThat(TestUtils.getPropertyValue(handler, "remoteFileTemplate.temporaryDirectoryExpressionProcessor")) .isNotNull(); assertThat(TestUtils.getPropertyValue(handler, "remoteFileTemplate.fileNameGenerator")) - .isEqualTo(context.getBean("fileNameGenerator")); - assertThat(TestUtils.getPropertyValue(handler, "remoteFileTemplate.charset")).isEqualTo("UTF-8"); - CachingSessionFactory sessionFactory = TestUtils.getPropertyValue(handler, - "remoteFileTemplate.sessionFactory", CachingSessionFactory.class); - DefaultSftpSessionFactory clientFactory = TestUtils.getPropertyValue(sessionFactory, "sessionFactory", - DefaultSftpSessionFactory.class); + .isEqualTo(this.context.getBean("fileNameGenerator")); + assertThat(TestUtils.getPropertyValue(handler, "remoteFileTemplate.charset")).isEqualTo(StandardCharsets.UTF_8); + CachingSessionFactory sessionFactory = + TestUtils.getPropertyValue(handler, "remoteFileTemplate.sessionFactory", CachingSessionFactory.class); + DefaultSftpSessionFactory clientFactory = + TestUtils.getPropertyValue(sessionFactory, "sessionFactory", DefaultSftpSessionFactory.class); assertThat(TestUtils.getPropertyValue(clientFactory, "host")).isEqualTo("localhost"); assertThat(TestUtils.getPropertyValue(clientFactory, "port")).isEqualTo(2222); assertThat(TestUtils.getPropertyValue(handler, "order")).isEqualTo(23); @@ -93,73 +102,68 @@ public class OutboundChannelAdapterParserTests { "handlers"); Iterator iterator = handlers.iterator(); assertThat(iterator.next()) - .isSameAs(TestUtils.getPropertyValue(context.getBean("sftpOutboundAdapterWithExpression"), "handler")); + .isSameAs(TestUtils.getPropertyValue( + this.context.getBean("sftpOutboundAdapterWithExpression"), "handler")); assertThat(iterator.next()).isSameAs(handler); assertThat(TestUtils.getPropertyValue(handler, "chmod")).isEqualTo(384); - context.close(); } @Test public void testOutboundChannelAdapterWithWithRemoteDirectoryAndFileExpression() { - ConfigurableApplicationContext context = - new ClassPathXmlApplicationContext("OutboundChannelAdapterParserTests-context.xml", this.getClass()); - Object consumer = context.getBean("sftpOutboundAdapterWithExpression"); - assertThat(consumer instanceof EventDrivenConsumer).isTrue(); - assertThat(TestUtils.getPropertyValue(consumer, "inputChannel")).isEqualTo(context.getBean("inputChannel")); - assertThat(((EventDrivenConsumer) consumer).getComponentName()).isEqualTo("sftpOutboundAdapterWithExpression"); - FileTransferringMessageHandler handler = TestUtils.getPropertyValue(consumer, "handler", FileTransferringMessageHandler.class); - SpelExpression remoteDirectoryExpression = (SpelExpression) TestUtils.getPropertyValue(handler, - "remoteFileTemplate.directoryExpressionProcessor.expression"); + EventDrivenConsumer consumer = + this.context.getBean("sftpOutboundAdapterWithExpression", EventDrivenConsumer.class); + assertThat(TestUtils.getPropertyValue(consumer, "inputChannel")) + .isEqualTo(this.context.getBean("inputChannel")); + assertThat(consumer.getComponentName()).isEqualTo("sftpOutboundAdapterWithExpression"); + FileTransferringMessageHandler handler = TestUtils + .getPropertyValue(consumer, "handler", FileTransferringMessageHandler.class); + SpelExpression remoteDirectoryExpression = + TestUtils.getPropertyValue(handler, "remoteFileTemplate.directoryExpressionProcessor.expression", + SpelExpression.class); assertThat(remoteDirectoryExpression).isNotNull(); assertThat(remoteDirectoryExpression.getExpressionString()).isEqualTo("'foo' + '/' + 'bar'"); - FileNameGenerator generator = (FileNameGenerator) TestUtils.getPropertyValue(handler, "remoteFileTemplate.fileNameGenerator"); + FileNameGenerator generator = + TestUtils.getPropertyValue(handler, "remoteFileTemplate.fileNameGenerator", FileNameGenerator.class); Expression fileNameGeneratorExpression = TestUtils.getPropertyValue(generator, "expression", Expression.class); assertThat(fileNameGeneratorExpression).isNotNull(); assertThat(fileNameGeneratorExpression.getExpressionString()).isEqualTo("payload.getName() + '-foo'"); - assertThat(TestUtils.getPropertyValue(handler, "remoteFileTemplate.charset")).isEqualTo("UTF-8"); + assertThat(TestUtils.getPropertyValue(handler, "remoteFileTemplate.charset")).isEqualTo(StandardCharsets.UTF_8); assertThat(TestUtils.getPropertyValue(handler, "remoteFileTemplate.temporaryDirectoryExpressionProcessor")) .isNull(); - context.close(); } @Test public void testOutboundChannelAdapterWithNoTemporaryFileName() { - ConfigurableApplicationContext context = - new ClassPathXmlApplicationContext("OutboundChannelAdapterParserTests-context.xml", this.getClass()); - Object consumer = context.getBean("sftpOutboundAdapterWithNoTemporaryFileName"); - FileTransferringMessageHandler handler = TestUtils.getPropertyValue(consumer, "handler", FileTransferringMessageHandler.class); + Object consumer = this.context.getBean("sftpOutboundAdapterWithNoTemporaryFileName"); + FileTransferringMessageHandler handler = TestUtils + .getPropertyValue(consumer, "handler", FileTransferringMessageHandler.class); assertThat((Boolean) TestUtils.getPropertyValue(handler, "remoteFileTemplate.useTemporaryFileName")).isFalse(); - context.close(); } @Test public void advised() { - ConfigurableApplicationContext context = - new ClassPathXmlApplicationContext("OutboundChannelAdapterParserTests-context.xml", this.getClass()); - Object consumer = context.getBean("advised"); + Object consumer = this.context.getBean("advised"); MessageHandler handler = TestUtils.getPropertyValue(consumer, "handler", MessageHandler.class); - handler.handleMessage(new GenericMessage("foo")); + handler.handleMessage(new GenericMessage<>("foo")); assertThat(adviceCalled).isEqualTo(1); - context.close(); } @Test public void testFailWithRemoteDirAndExpression() { - try { - new ClassPathXmlApplicationContext("OutboundChannelAdapterParserTests-context-fail.xml", this.getClass()) - .close(); - fail("Exception expected"); - } - catch (BeanDefinitionStoreException e) { - assertThat(e.getMessage()).contains("Only one of 'remote-directory'"); - } - + assertThatExceptionOfType(BeanDefinitionStoreException.class) + .isThrownBy(() -> + new ClassPathXmlApplicationContext("OutboundChannelAdapterParserTests-context-fail.xml", + getClass())) + .withMessageContaining("Only one of 'remote-directory'"); } - @Test(expected = BeanDefinitionStoreException.class) + @Test public void testFailWithFileExpressionAndFileGenerator() { - new ClassPathXmlApplicationContext("OutboundChannelAdapterParserTests-context-fail-fileFileGen.xml", - this.getClass()).close(); + assertThatExceptionOfType(BeanDefinitionStoreException.class) + .isThrownBy(() -> + new ClassPathXmlApplicationContext( + "OutboundChannelAdapterParserTests-context-fail-fileFileGen.xml", + getClass())); } public static class FooAdvice extends AbstractRequestHandlerAdvice { @@ -171,4 +175,5 @@ public class OutboundChannelAdapterParserTests { } } + } diff --git a/src/reference/asciidoc/ftp.adoc b/src/reference/asciidoc/ftp.adoc index 1355add3b3..4983ce66f7 100644 --- a/src/reference/asciidoc/ftp.adoc +++ b/src/reference/asciidoc/ftp.adoc @@ -821,7 +821,9 @@ The FTP outbound channel adapter supports the following payloads: * `java.io.File`: The actual file object * `byte[]`: A byte array that represents the file contents -* `java.lang.String`: Text that represents the file contents. +* `java.lang.String`: Text that represents the file contents +* `java.io.InputStream`: a stream of data to transfer to remote file +* `org.springframework.core.io.Resource`: a resource for data to transfer to remote file The following example shows how to configure an `outbound-channel-adapter`: @@ -859,7 +861,9 @@ By default, an existing file is overwritten. The modes are defined by the `FileExistsMode` enumeration, which includes the following values: * `REPLACE` (default) -*`APPEND` +* `REPLACE_IF_MODIFIED` +* `APPEND` +* `APPEND_NO_FLUSH` * `IGNORE` * `FAIL` diff --git a/src/reference/asciidoc/sftp.adoc b/src/reference/asciidoc/sftp.adoc index 0f0a2b262c..d553c45aec 100644 --- a/src/reference/asciidoc/sftp.adoc +++ b/src/reference/asciidoc/sftp.adoc @@ -822,6 +822,8 @@ Similar to the FTP outbound adapter, the SFTP outbound channel adapter supports * `java.io.File`: The actual file object * `byte[]`: A byte array that represents the file contents * `java.lang.String`: Text that represents the file contents +* `java.io.InputStream`: a stream of data to transfer to remote file +* `org.springframework.core.io.Resource`: a resource for data to transfer to remote file The following example shows how to configure an SFTP outbound channel adapter: @@ -852,7 +854,15 @@ In the preceding example, we define the `remote-filename-generator-expression` a Starting with version 4.1, you can specify the `mode` when you transferring the file. By default, an existing file is overwritten. -The modes are defined by the `FileExistsMode` enumeration, which has the following values: `REPLACE` (default), `APPEND`, `IGNORE`, and `FAIL`. +The modes are defined by the `FileExistsMode` enumeration, which includes the following values: + +* `REPLACE` (default) +* `REPLACE_IF_MODIFIED` +* `APPEND` +* `APPEND_NO_FLUSH` +* `IGNORE` +* `FAIL` + With `IGNORE` and `FAIL`, the file is not transferred. `FAIL` causes an exception to be thrown, while `IGNORE` silently ignores the transfer (although a `DEBUG` log entry is produced). diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 409a671f14..8bd5e1e82f 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -126,3 +126,9 @@ See <<./mqtt.adoc#mqtt-ack-mode,Manual Acks>> for more information. The outbound adapter now publishes a `MqttConnectionFailedEvent` when a connection can't be created, or is lost. Previously, only the inbound adapter did so. See <<./mqtt.adoc#mqtt-events,MQTT Events>>. + +[[x5.3-sftp]] +=== (S)FTP Changes + +The `FileTransferringMessageHandler` (for FTP and SFTP, for example) in addition to `File`, `byte[]`, `String` and `InputStream` now also supports an `org.springframework.core.io.Resource`. +See <<./sftp.adoc#sftp,SFTP Support>> and <<./ftp.adoc#ftp,FTP Support>> for more information.