Repository: mina-sshd
Updated Branches:
  refs/heads/master 02f1d57be -> c9f8995c4


[SSHD-741] Provide seamless replacement for Spring integation SFTP adapter


Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/c9f8995c
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/c9f8995c
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/c9f8995c

Branch: refs/heads/master
Commit: c9f8995c41411d0f108341b4ec5600f737d1c1a7
Parents: 283857b
Author: Goldstein Lyor <l...@c-b4.com>
Authored: Thu May 25 11:58:30 2017 +0300
Committer: Goldstein Lyor <l...@c-b4.com>
Committed: Sun Jun 4 12:30:12 2017 +0300

----------------------------------------------------------------------
 pom.xml                                         |   8 +-
 .../subsystem/AbstractSubsystemClient.java      |   8 +
 .../sshd/client/subsystem/sftp/SftpClient.java  |  33 ++
 .../subsystem/sftp/SftpVersionSelector.java     |  23 +-
 .../apache/sshd/common/util/GenericUtils.java   |  44 +-
 .../apache/sshd/client/ClientDeadlockTest.java  |   7 +-
 sshd-spring-sftp/pom.xml                        | 133 ++++++
 .../sftp/ApacheSshdSftpSessionFactory.java      | 473 +++++++++++++++++++
 .../integration/sftp/SpringSftpSession.java     | 262 ++++++++++
 .../sftp/ApacheSshdSftpSessionFactoryTest.java  | 400 ++++++++++++++++
 10 files changed, 1358 insertions(+), 33 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c9f8995c/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index 06be0ad..470115e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -110,7 +110,7 @@
 
         <bouncycastle.version>1.57</bouncycastle.version>
         <slf4j.version>1.7.25</slf4j.version>
-        <spring.version>3.2.17.RELEASE</spring.version>
+        <spring.version>4.3.8.RELEASE</spring.version>
         <jgit.version>4.7.0.201704051617-r</jgit.version>
         <junit.version>4.12</junit.version>
         <surefire.plugin.version>2.20</surefire.plugin.version>
@@ -296,11 +296,6 @@
                 </exclusions>
             </dependency>
             <dependency>
-                <groupId>commons-logging</groupId>
-                <artifactId>commons-logging</artifactId>
-                <version>1.1.1</version>
-            </dependency>
-            <dependency>
                 <groupId>log4j</groupId>
                 <artifactId>log4j</artifactId>
                 <version>1.2.17</version>
@@ -1007,6 +1002,7 @@
         <module>sshd-ldap</module>
         <module>sshd-git</module>
         <module>sshd-contrib</module>
+        <module>sshd-spring-sftp</module>
         <module>assembly</module>
     </modules>
 </project>

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c9f8995c/sshd-core/src/main/java/org/apache/sshd/client/subsystem/AbstractSubsystemClient.java
----------------------------------------------------------------------
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/AbstractSubsystemClient.java
 
b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/AbstractSubsystemClient.java
index 6bd32c1..32e8d26 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/AbstractSubsystemClient.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/AbstractSubsystemClient.java
@@ -34,4 +34,12 @@ public abstract class AbstractSubsystemClient extends 
AbstractLoggingBean implem
     public final ClientSession getSession() {
         return getClientSession();
     }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName()
+            + "[name=" + getName()
+            + ", session=" + getSession()
+            + "]";
+    }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c9f8995c/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
----------------------------------------------------------------------
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java 
b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
index bda565d..eb22451 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
@@ -30,6 +30,7 @@ import java.nio.file.attribute.FileTime;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
@@ -443,6 +444,36 @@ public interface SftpClient extends SubsystemClient {
     }
 
     class DirEntry {
+        public static final Comparator<DirEntry> BY_CASE_SENSITIVE_FILENAME = 
new Comparator<DirEntry>() {
+            @Override
+            public int compare(DirEntry o1, DirEntry o2) {
+                if (o1 == o2) {
+                    return 0;
+                } else if (o1 == null) {
+                    return 1;
+                } else if (o2 == null) {
+                    return -1;
+                } else {
+                    return GenericUtils.safeCompare(o1.getFilename(), 
o2.getFilename(), true);
+                }
+            }
+        };
+
+        public static final Comparator<DirEntry> BY_CASE_INSENSITIVE_FILENAME 
= new Comparator<DirEntry>() {
+            @Override
+            public int compare(DirEntry o1, DirEntry o2) {
+                if (o1 == o2) {
+                    return 0;
+                } else if (o1 == null) {
+                    return 1;
+                } else if (o2 == null) {
+                    return -1;
+                } else {
+                    return GenericUtils.safeCompare(o1.getFilename(), 
o2.getFilename(), false);
+                }
+            }
+        };
+
         private final String filename;
         private final String longFilename;
         private final Attributes attributes;
@@ -471,6 +502,8 @@ public interface SftpClient extends SubsystemClient {
         }
     }
 
+    DirEntry[] EMPTY_DIR_ENTRIES = new DirEntry[0];
+
     // default values used if none specified
     int MIN_BUFFER_SIZE = Byte.MAX_VALUE;
     int MIN_READ_BUFFER_SIZE = MIN_BUFFER_SIZE;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c9f8995c/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpVersionSelector.java
----------------------------------------------------------------------
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpVersionSelector.java
 
b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpVersionSelector.java
index 83a7964..3f0de71 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpVersionSelector.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpVersionSelector.java
@@ -33,7 +33,6 @@ import org.apache.sshd.common.util.ValidateUtils;
  */
 @FunctionalInterface
 public interface SftpVersionSelector {
-
     /**
      * An {@link SftpVersionSelector} that returns the current version
      */
@@ -54,8 +53,7 @@ public interface SftpVersionSelector {
     /**
      * @param session   The {@link ClientSession} through which the SFTP 
connection is made
      * @param current   The current version negotiated with the server
-     * @param available Extra versions available - may be empty and/or contain
-     *                  only the current one
+     * @param available Extra versions available - may be empty and/or contain 
only the current one
      * @return The new requested version - if same as current, then nothing is 
done
      */
     int selectVersion(ClientSession session, int current, List<Integer> 
available);
@@ -69,8 +67,8 @@ public interface SftpVersionSelector {
      * @param version The requested version
      * @return The {@link SftpVersionSelector}
      */
-    static SftpVersionSelector fixedVersionSelector(final int version) {
-        return (session, current, available) -> version;
+    static SftpVersionSelector fixedVersionSelector(int version) {
+        return new NamedVersionSelector(Integer.toString(version), (session, 
current, available) -> version);
     }
 
     /**
@@ -83,7 +81,7 @@ public interface SftpVersionSelector {
      * @return A {@link SftpVersionSelector} that attempts to select
      * the most preferred version that is also listed as available.
      */
-    static SftpVersionSelector preferredVersionSelector(final int... 
preferred) {
+    static SftpVersionSelector preferredVersionSelector(int... preferred) {
         return preferredVersionSelector(NumberUtils.asList(preferred));
 
     }
@@ -98,13 +96,13 @@ public interface SftpVersionSelector {
      * @return A {@link SftpVersionSelector} that attempts to select
      * the most preferred version that is also listed as available.
      */
-    static SftpVersionSelector preferredVersionSelector(final Iterable<? 
extends Number> preferred) {
+    static SftpVersionSelector preferredVersionSelector(Iterable<? extends 
Number> preferred) {
         ValidateUtils.checkNotNullAndNotEmpty((Collection<?>) preferred, 
"Empty preferred versions");
-        return (session, current, available) -> 
StreamSupport.stream(preferred.spliterator(), false)
-                .mapToInt(Number::intValue)
-                .filter(v -> v == current || available.contains(v))
-                .findFirst()
-                .orElseThrow(() -> new IllegalStateException("Preferred 
versions (" + preferred + ") not available: " + available));
+        return new NamedVersionSelector(GenericUtils.join(preferred, ','), 
(session, current, available) -> StreamSupport.stream(preferred.spliterator(), 
false)
+            .mapToInt(Number::intValue)
+            .filter(v -> v == current || available.contains(v))
+            .findFirst()
+            .orElseThrow(() -> new IllegalStateException("Preferred versions 
(" + preferred + ") not available: " + available)));
     }
 
     class NamedVersionSelector implements SftpVersionSelector {
@@ -126,5 +124,4 @@ public interface SftpVersionSelector {
             return name;
         }
     }
-
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c9f8995c/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java
----------------------------------------------------------------------
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java 
b/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java
index 1f56fec..bd8a7a3 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java
@@ -39,6 +39,7 @@ import java.util.SortedMap;
 import java.util.SortedSet;
 import java.util.TreeMap;
 import java.util.TreeSet;
+import java.util.concurrent.ExecutionException;
 import java.util.function.BinaryOperator;
 import java.util.function.Consumer;
 import java.util.function.Function;
@@ -411,24 +412,18 @@ public final class GenericUtils {
         return stream(values).map(mapper).collect(Collectors.toList());
     }
 
-    public static <T, U> SortedSet<U> mapSort(Collection<T> values,
-                                              Function<? super T, ? extends U> 
mapper,
-                                              Comparator<U> comparator) {
+    public static <T, U> SortedSet<U> mapSort(
+            Collection<T> values, Function<? super T, ? extends U> mapper, 
Comparator<U> comparator) {
         return stream(values).map(mapper).collect(toSortedSet(comparator));
     }
 
     public static <T, K, U> SortedMap<K, U> toSortedMap(
-                                Iterable<T> values,
-                                Function<? super T, ? extends K> keyMapper,
-                                Function<? super T, ? extends U> valueMapper,
-                                Comparator<K> comparator) {
+            Iterable<T> values, Function<? super T, ? extends K> keyMapper, 
Function<? super T, ? extends U> valueMapper, Comparator<K> comparator) {
         return stream(values).collect(toSortedMap(keyMapper, valueMapper, 
comparator));
     }
 
     public static <T, K, U> Collector<T, ?, SortedMap<K, U>> toSortedMap(
-                                Function<? super T, ? extends K> keyMapper,
-                                Function<? super T, ? extends U> valueMapper,
-                                Comparator<K> comparator) {
+            Function<? super T, ? extends K> keyMapper, Function<? super T, ? 
extends U> valueMapper, Comparator<K> comparator) {
         return Collectors.toMap(keyMapper, valueMapper, throwingMerger(), () 
-> new TreeMap<>(comparator));
     }
 
@@ -657,6 +652,29 @@ public final class GenericUtils {
         }
     }
 
+    public static RuntimeException toRuntimeException(Throwable t) {
+        return toRuntimeException(t, true);
+    }
+
+    /**
+     * Converts a thrown generic exception to a {@link RuntimeException}
+     *
+     * @param t The original thrown exception
+     * @param peelThrowable Whether to determine the root cause by 
&quot;peeling&quot;
+     * any enclosing exceptions
+     * @return The thrown cause if already a runtime exception, otherwise a
+     * runtime exception of the resolved exception as its cause
+     * @see #peelException(Throwable)
+     */
+    public static RuntimeException toRuntimeException(Throwable t, boolean 
peelThrowable) {
+        Throwable e = peelThrowable ? peelException(t) : t;
+        if (e instanceof RuntimeException) {
+            return (RuntimeException) e;
+        }
+
+        return new RuntimeException(e);
+    }
+
     /**
      * Attempts to get to the &quot;effective&quot; exception being thrown,
      * by taking care of some known exceptions that wrap the original thrown
@@ -666,6 +684,7 @@ public final class GenericUtils {
      * @return The effective exception - same as input if not a wrapper
      */
     public static Throwable peelException(Throwable t) {
+        // NOTE: check order is important - e.g., InvocationTargetException 
extends ReflectiveOperationException
         if (t == null) {
             return t;
         } else if (t instanceof UndeclaredThrowableException) {
@@ -690,6 +709,11 @@ public final class GenericUtils {
             if (target != null) {
                 return peelException(target);
             }
+        } else if (t instanceof ExecutionException) {
+            Throwable wrapped = resolveExceptionCause(t);
+            if (wrapped != null) {
+                return peelException(wrapped);
+            }
         } else if (t instanceof MBeanException) {
             Throwable target = ((MBeanException) t).getTargetException();
             if (target != null) {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c9f8995c/sshd-core/src/test/java/org/apache/sshd/client/ClientDeadlockTest.java
----------------------------------------------------------------------
diff --git 
a/sshd-core/src/test/java/org/apache/sshd/client/ClientDeadlockTest.java 
b/sshd-core/src/test/java/org/apache/sshd/client/ClientDeadlockTest.java
index bff2420..9c37ba4 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/ClientDeadlockTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/ClientDeadlockTest.java
@@ -78,9 +78,8 @@ public class ClientDeadlockTest extends BaseTestSupport {
         client.start();
 
         ConnectFuture future = client.connect(getCurrentTestName(), 
TEST_LOCALHOST, port);
-        ClientSession session = future.verify(5, 
TimeUnit.SECONDS).getSession();
-        session.waitFor(EnumSet.of(ClientSession.ClientSessionEvent.CLOSED), 
TimeUnit.SECONDS.toMillis(3L));
-        assertTrue(session.isClosed());
+        ClientSession session = future.verify(5L, 
TimeUnit.SECONDS).getSession();
+        session.waitFor(EnumSet.of(ClientSession.ClientSessionEvent.CLOSED), 
TimeUnit.SECONDS.toMillis(7L));
+        assertFalse(session.isOpen());
     }
-
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c9f8995c/sshd-spring-sftp/pom.xml
----------------------------------------------------------------------
diff --git a/sshd-spring-sftp/pom.xml b/sshd-spring-sftp/pom.xml
new file mode 100644
index 0000000..e3d00f8
--- /dev/null
+++ b/sshd-spring-sftp/pom.xml
@@ -0,0 +1,133 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/maven-v4_0_0.xsd";>
+
+
+    <!--
+
+        Licensed to the Apache Software Foundation (ASF) under one or more
+        contributor license agreements.  See the NOTICE file distributed with
+        this work for additional information regarding copyright ownership.
+        The ASF licenses this file to You 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.
+    -->
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.sshd</groupId>
+        <artifactId>sshd</artifactId>
+        <version>1.6.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>sshd-spring-sftp</artifactId>
+    <name>Apache Mina SSHD :: Spring integration SFTP adapter</name>
+    <packaging>jar</packaging>
+    <inceptionYear>2017</inceptionYear>
+
+    <properties>
+        <projectRoot>${basedir}/..</projectRoot>
+        <spring.integration.version>4.3.9.RELEASE</spring.integration.version>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.springframework.integration</groupId>
+                <artifactId>spring-integration-stream</artifactId>
+                <version>${spring.integration.version}</version>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.sshd</groupId>
+            <artifactId>sshd-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <!-- Replacement of commons-logging for Spring parts that still use it 
-->
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>jcl-over-slf4j</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.integration</groupId>
+            <artifactId>spring-integration-core</artifactId>
+            <version>${spring.integration.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.integration</groupId>
+            <artifactId>spring-integration-file</artifactId>
+            <version>${spring.integration.version}</version>
+        </dependency>
+
+            <!-- Test dependencies -->
+        <dependency>
+            <groupId>org.springframework.integration</groupId>
+            <artifactId>spring-integration-sftp</artifactId>
+            <version>${spring.integration.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.jcraft</groupId>
+            <artifactId>jsch</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.jcraft</groupId>
+            <artifactId>jzlib</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sshd</groupId>
+            <artifactId>sshd-core</artifactId>
+            <version>${project.version}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-log4j12</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <redirectTestOutputToFile>true</redirectTestOutputToFile>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <configuration>
+                    <additionalparam>-Xdoclint:none</additionalparam>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c9f8995c/sshd-spring-sftp/src/main/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactory.java
----------------------------------------------------------------------
diff --git 
a/sshd-spring-sftp/src/main/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactory.java
 
b/sshd-spring-sftp/src/main/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactory.java
new file mode 100644
index 0000000..cb455cd
--- /dev/null
+++ 
b/sshd-spring-sftp/src/main/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactory.java
@@ -0,0 +1,473 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.sshd.spring.integration.sftp;
+
+import java.io.InputStream;
+import java.security.KeyPair;
+import java.security.PublicKey;
+import java.util.Collection;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.future.ConnectFuture;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.client.simple.SimpleClientConfigurator;
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient.DirEntry;
+import org.apache.sshd.client.subsystem.sftp.SftpVersionSelector;
+import org.apache.sshd.common.PropertyResolverUtils;
+import org.apache.sshd.common.config.SshConfigFileReader;
+import org.apache.sshd.common.config.keys.FilePasswordProvider;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.loader.pem.PEMResourceParserUtils;
+import org.apache.sshd.common.future.CloseFuture;
+import org.apache.sshd.common.future.SshFutureListener;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+import org.apache.sshd.server.subsystem.sftp.SftpSubsystem;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.core.io.Resource;
+import org.springframework.integration.file.remote.session.Session;
+import org.springframework.integration.file.remote.session.SessionFactory;
+import 
org.springframework.integration.file.remote.session.SharedSessionCapable;
+
+/**
+ * A proper replacement for the {@link 
org.springframework.integration.sftp.session.DefaultSftpSessionFactory}
+ *
+ * @author <a href="mailto:d...@mina.apache.org";>Apache MINA SSHD Project</a>
+ */
+public class ApacheSshdSftpSessionFactory
+        extends AbstractLoggingBean
+        implements SessionFactory<DirEntry>, SharedSessionCapable,
+        SimpleClientConfigurator,
+        InitializingBean, DisposableBean {
+
+    // TODO add support for loading multiple private keys
+    protected volatile KeyPair privateKeyPair;
+
+    private final boolean sharedSession;
+    private final AtomicReference<ClientSession> sharedSessionHolder = new 
AtomicReference<>();
+
+    private volatile String hostValue;
+    private volatile int portValue = SshConfigFileReader.DEFAULT_PORT;
+    private volatile String userValue;
+    private volatile String passwordValue;
+    private volatile Resource privateKey;
+    private volatile String privateKeyPassphrase;
+    private volatile Properties sessionConfig;
+    private volatile long connTimeout = DEFAULT_CONNECT_TIMEOUT;
+    private volatile long authTimeout = DEFAULT_AUTHENTICATION_TIMEOUT;
+    private volatile SftpVersionSelector versionSelector = 
SftpVersionSelector.CURRENT;
+
+    private SshClient sshClient;
+
+    public ApacheSshdSftpSessionFactory() {
+        this(false);
+    }
+
+    public ApacheSshdSftpSessionFactory(boolean sharedSession) {
+        this.sharedSession = sharedSession;
+    }
+
+    public String getHost() {
+        return hostValue;
+    }
+
+    /**
+     * @param host The host to connect to - this is a mandatory property.
+     */
+    public void setHost(String host) {
+        this.hostValue = ValidateUtils.checkNotNullAndNotEmpty(host, "No host 
name provided");
+    }
+
+    public int getPort() {
+        return portValue;
+    }
+
+    /**
+     * The port over which the SFTP connection shall be established. If not 
specified,
+     * this value defaults to <code>22</code>. If specified, this property must
+     * be a positive number.
+     *
+     * @param port The port value
+     */
+    public void setPort(int port) {
+        ValidateUtils.checkTrue(port > 0, "Non-positive port value specified: 
%d", port);
+        this.portValue = port;
+    }
+
+    public String getUser() {
+        return userValue;
+    }
+
+    /**
+     * The remote user to use. This is a mandatory property.
+     *
+     * @param user The username
+     */
+    public void setUser(String user) {
+        this.userValue = ValidateUtils.checkNotNullAndNotEmpty(user, "No user 
specified: %s", user);
+    }
+
+    public String getPassword() {
+        return passwordValue;
+    }
+
+    /**
+     * The password to authenticate against the remote host. If a password is
+     * not provided, then a {@link #setPrivateKey(Resource)} call is mandatory.
+     *
+     * @param password The password to use - if {@code null} then no password
+     * is set - in which case the {@link #getPrivateKey()} resource is used
+     */
+    public void setPassword(String password) {
+        this.passwordValue = password;
+    }
+
+    public Resource getPrivateKey() {
+        return privateKey;
+    }
+
+    /**
+     * Allows you to set a {@link Resource}, which represents the location of 
the
+     * private key used for authenticating against the remote host. If the 
privateKey
+     * is not provided, then the {@link #setPassword(String)} call is mandatory
+     *
+     * @param privateKey The private key {@link Resource}
+     */
+    public void setPrivateKey(Resource privateKey) {
+        this.privateKey = privateKey;
+    }
+
+    public String getPrivateKeyPassphrase() {
+        return privateKeyPassphrase;
+    }
+
+    /**
+     * @param privateKeyPassphrase The password for the private key - required 
if
+     * the private key resource is encrypted
+     */
+    public void setPrivateKeyPassphrase(String privateKeyPassphrase) {
+        this.privateKeyPassphrase = privateKeyPassphrase;
+    }
+
+    public KeyPair getPrivateKeyPair() {
+        return privateKeyPair;
+    }
+
+    public void setPrivateKeyPair(KeyPair privateKeyPair) {
+        this.privateKeyPair = privateKeyPair;
+    }
+
+    @Override   // In seconds
+    public long getConnectTimeout() {
+        return connTimeout;
+    }
+
+    @Override
+    public void setConnectTimeout(long timeout) {
+        connTimeout = timeout;
+    }
+
+    @Override   // In seconds
+    public long getAuthenticationTimeout() {
+        return authTimeout;
+    }
+
+    @Override
+    public void setAuthenticationTimeout(long timeout) {
+        authTimeout = timeout;
+    }
+
+    public Properties getSessionConfig() {
+        return sessionConfig;
+    }
+
+    /**
+     * @param sessionConfig Extra {@link Properties} that can be used to set 
specific
+     * SSHD session properties
+     */
+    public void setSessionConfig(Properties sessionConfig) {
+        this.sessionConfig = sessionConfig;
+    }
+
+    public SshClient getSshClient() {
+        return sshClient;
+    }
+
+    public void setSshClient(SshClient sshClient) {
+        this.sshClient = sshClient;
+    }
+
+    @Override
+    public boolean isSharedSession() {
+        return sharedSession;
+    }
+
+    public SftpVersionSelector getSftpVersionSelector() {
+        return versionSelector;
+    }
+
+    public void setSftpVersion(String version) {
+        if ("CURRENT".equalsIgnoreCase(version)) {
+            setSftpVersionSelector(SftpVersionSelector.CURRENT);
+        } else if ("MAXIMUM".equalsIgnoreCase(version)) {
+            setSftpVersionSelector(SftpVersionSelector.MAXIMUM);
+        } else if ("MINIMUM".equalsIgnoreCase(version)) {
+            setSftpVersionSelector(SftpVersionSelector.MINIMUM);
+        } else {
+            int fixedVersion = Integer.parseInt(version);
+            ValidateUtils.checkTrue((fixedVersion >= 
SftpSubsystem.LOWER_SFTP_IMPL) && (fixedVersion <= 
SftpSubsystem.HIGHER_SFTP_IMPL),
+                    "Unsupported SFTP version: %s", version);
+            
setSftpVersionSelector(SftpVersionSelector.fixedVersionSelector(fixedVersion));
+        }
+    }
+
+    public void setSftpVersionSelector(SftpVersionSelector selector) {
+        versionSelector = Objects.requireNonNull(selector, "No version 
selector provided");
+    }
+
+    protected ClientSession getSharedClientSession() {
+        synchronized (sharedSessionHolder) {
+            return sharedSessionHolder.get();
+        }
+    }
+
+    @Override
+    public void resetSharedSession() {
+        ClientSession sharedSession;
+        synchronized (sharedSessionHolder) {
+            sharedSession = sharedSessionHolder.getAndSet(null);
+        }
+        if (sharedSession != null) {
+            log.info("resetSharedSession - session={}", sharedSession);
+            sharedSession.close(false).addListener(new 
SshFutureListener<CloseFuture>() {
+                @SuppressWarnings("synthetic-access")
+                @Override
+                public void operationComplete(CloseFuture future) {
+                    log.info("resetSharedSession - session closed: {}", 
sharedSession);
+                }
+            });
+        }
+    }
+
+    @Override
+    public void afterPropertiesSet() throws Exception {
+        KeyPair kp = getPrivateKeyPair();
+        if (kp == null) {
+            Resource privateKeyLocation = getPrivateKey();
+            if (privateKeyLocation != null) {
+                kp = loadPrivateKey(privateKeyLocation, 
getPrivateKeyPassphrase());
+                log.info("afterPropertiesSet() - loaded private key={}", 
privateKeyLocation);
+                setPrivateKeyPair(kp);
+            }
+        }
+        ValidateUtils.checkState(GenericUtils.isNotEmpty(getPassword()) || (kp 
!= null), "Either password or private key must be set");
+
+        SshClient client = getSshClient();
+        if (client == null) {
+            client = createSshClientInstance();
+            setSshClient(client);
+        }
+
+        if (!client.isOpen()) {
+            log.info("afterPropertiesSet() - starting client");
+            client.start();
+            log.info("afterPropertiesSet() - client started");
+        }
+    }
+
+    protected SshClient createSshClientInstance() throws Exception {
+        return SshClient.setUpDefaultClient();
+    }
+
+    @Override
+    public void destroy() throws Exception {
+        SshClient client = getSshClient();
+        if ((client != null) && client.isOpen()) {
+            log.info("destroy() - stopping client");
+            client.close(false);    // do not wait for the close to complete
+            log.info("destroy() - client stopped");
+        }
+    }
+
+    protected KeyPair loadPrivateKey(Resource keyResource, String keyPassword) 
throws Exception {
+        FilePasswordProvider passwordProvider = 
GenericUtils.isEmpty(keyPassword)
+                ? FilePasswordProvider.EMPTY
+                : FilePasswordProvider.of(keyPassword);
+        Collection<KeyPair> keyPairs;
+        try (InputStream inputStream = keyResource.getInputStream()) {
+            keyPairs = 
PEMResourceParserUtils.PROXY.loadKeyPairs(keyResource.toString(), 
passwordProvider, inputStream);
+        }
+
+        int numLoaded = GenericUtils.size(keyPairs);
+        ValidateUtils.checkState(numLoaded > 0, "No keys loaded from %s", 
keyResource);
+        ValidateUtils.checkState(numLoaded == 1, "Multiple keys loaded from 
%s", keyResource);
+        return keyPairs.iterator().next();
+    }
+
+    @Override
+    public Session<DirEntry> getSession() {
+        boolean sharedInstance = isSharedSession();
+        try {
+            ClientSession session = null;
+            try {
+                session = resolveClientSession(sharedInstance);
+
+                SftpVersionSelector selector = getSftpVersionSelector();
+                SftpClient sftpClient = session.createSftpClient(selector);
+                try {
+                    ClientSession sessionInstance = session;
+                    Session<DirEntry> result = sharedInstance
+                        ? new SpringSftpSession(sftpClient)
+                        : new SpringSftpSession(sftpClient, () -> {
+                            try {
+                                sessionInstance.close();
+                                return null;
+                            } catch (Exception e) {
+                                return e;
+                            }
+                        });
+                    // avoid auto-close at finally clause
+                    sftpClient = null;
+                    session = null;
+                    return result;
+                } finally {
+                    if (sftpClient != null) {
+                        sftpClient.close();
+                    }
+                }
+            } finally {
+                if (session != null) {
+                    try {
+                        session.close();
+                    } finally {
+                        if (sharedInstance) {
+                            resetSharedSession();
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            throw GenericUtils.toRuntimeException(e);
+        }
+    }
+
+    protected ClientSession resolveClientSession(boolean sharedInstance) 
throws Exception {
+        ClientSession session;
+        if (sharedInstance) {
+            synchronized (sharedSessionHolder) {
+                session = sharedSessionHolder.get();
+                if (session == null) {
+                    session = createClientSession();
+                }
+                sharedSessionHolder.set(session);
+            }
+        } else {
+            session = createClientSession();
+        }
+
+        return session;
+    }
+
+    protected ClientSession createClientSession() throws Exception {
+        String hostname = ValidateUtils.checkNotNullAndNotEmpty(getHost(), 
"Host must not be empty");
+        String username = ValidateUtils.checkNotNullAndNotEmpty(getUser(), 
"User must not be empty");
+        String passwordIdentity = getPassword();
+        KeyPair kp = getPrivateKeyPair();
+        ValidateUtils.checkState(GenericUtils.isNotEmpty(passwordIdentity) || 
(kp != null),
+                "Either password or private key must be set");
+        ClientSession session = createClientSession(hostname, username, 
getPort(), getEffectiveTimeoutValue(getConnectTimeout()));
+        try {
+            session = configureClientSessionProperties(session, 
getSessionConfig());
+            session = authenticateClientSession(session, passwordIdentity, kp, 
getEffectiveTimeoutValue(getAuthenticationTimeout()));
+
+            ClientSession newSession = session;
+            if (log.isDebugEnabled()) {
+                log.debug("createClientSession - session={}", session);
+            }
+            session = null; // avoid auto-close at finally clause
+            return newSession;
+        } finally {
+            if (session != null) {
+                session.close();
+            }
+        }
+    }
+
+    protected ClientSession createClientSession(String hostname, String 
username, int port, long timeout) throws Exception {
+        SshClient client = getSshClient();
+        if (log.isDebugEnabled()) {
+            log.debug("createClientSession({}@{}:{}) waitTimeout={}", 
username, hostname, port, timeout);
+        }
+        ConnectFuture connectFuture = client.connect(username, hostname, port);
+        return connectFuture.verify(timeout).getSession();
+    }
+
+    protected ClientSession configureClientSessionProperties(ClientSession 
session, Properties props) throws Exception {
+        if (GenericUtils.isEmpty(props)) {
+            return session;
+        }
+
+        boolean debugEnabled = log.isDebugEnabled();
+        for (String propName : props.stringPropertyNames()) {
+            String propValue = props.getProperty(propName);
+            if (debugEnabled) {
+                log.debug("configureClientSessionProperties({}) set {}={}", 
session, propName, propValue);
+            }
+            PropertyResolverUtils.updateProperty(session, propName, propValue);
+        }
+
+        return session;
+    }
+
+    protected ClientSession authenticateClientSession(
+            ClientSession session, String passwordIdentity, KeyPair 
privateKeyIdentity, long timeout) throws Exception {
+        if (log.isDebugEnabled()) {
+            PublicKey key = (privateKeyIdentity == null) ? null : 
privateKeyIdentity.getPublic();
+            log.debug("authenticateClientSession({}) password?={}, key={}/{}",
+                    session, GenericUtils.isNotEmpty(passwordIdentity), 
KeyUtils.getKeyType(key), KeyUtils.getFingerPrint(key));
+        }
+
+        if (GenericUtils.isNotEmpty(passwordIdentity)) {
+            session.addPasswordIdentity(passwordIdentity);
+        }
+
+        if (privateKeyIdentity != null) {
+            session.addPublicKeyIdentity(privateKeyIdentity);
+        }
+
+        session.auth().verify(timeout);
+        return session;
+    }
+
+    protected long getEffectiveTimeoutValue(long timeoutSeconds) {
+        if (timeoutSeconds < (Long.MAX_VALUE / 61L)) {
+            return TimeUnit.SECONDS.toMillis(timeoutSeconds);
+        } else {
+            return timeoutSeconds;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c9f8995c/sshd-spring-sftp/src/main/java/org/apache/sshd/spring/integration/sftp/SpringSftpSession.java
----------------------------------------------------------------------
diff --git 
a/sshd-spring-sftp/src/main/java/org/apache/sshd/spring/integration/sftp/SpringSftpSession.java
 
b/sshd-spring-sftp/src/main/java/org/apache/sshd/spring/integration/sftp/SpringSftpSession.java
new file mode 100644
index 0000000..63c0883
--- /dev/null
+++ 
b/sshd-spring-sftp/src/main/java/org/apache/sshd/spring/integration/sftp/SpringSftpSession.java
@@ -0,0 +1,262 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.sshd.spring.integration.sftp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.Objects;
+import java.util.concurrent.Callable;
+import java.util.stream.Collectors;
+
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient.Attributes;
+import org.apache.sshd.client.subsystem.sftp.SftpClient.DirEntry;
+import org.apache.sshd.client.subsystem.sftp.SftpClient.OpenMode;
+import org.apache.sshd.common.subsystem.sftp.SftpException;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+import org.springframework.integration.file.remote.session.Session;
+import org.springframework.util.FileCopyUtils;
+
+/**
+ * Implements the <I>Spring</I> session for the SFTP client
+ *
+ * @author <a href="mailto:d...@mina.apache.org";>Apache MINA SSHD Project</a>
+ */
+public class SpringSftpSession extends AbstractLoggingBean implements 
Session<DirEntry> {
+    private final SftpClient sftpClient;
+    private final Callable<Exception> sessionCloser;
+
+    public SpringSftpSession(SftpClient clientInstance) {
+        this(clientInstance, () -> null);
+    }
+
+    public SpringSftpSession(SftpClient clientInstance, Callable<Exception> 
sessionCloser) {
+        this.sftpClient = Objects.requireNonNull(clientInstance, "No SFTP 
client instance");
+        this.sessionCloser = sessionCloser;
+    }
+
+    @Override
+    public boolean isOpen() {
+        SftpClient client = getClientInstance();
+        return client.isOpen();
+    }
+
+    @Override
+    public SftpClient getClientInstance() {
+        return sftpClient;
+    }
+
+    public Callable<Exception> getSessionCloser() {
+        return sessionCloser;
+    }
+
+    @Override
+    public void close() {
+        Exception err = null;
+        try {
+            SftpClient client = getClientInstance();
+            closeClientInstance(client);
+        } catch (Exception e) {
+            err = GenericUtils.accumulateException(err, e);
+        }
+
+        try {
+            closeSessionInstance(getSessionCloser());
+        } catch (Exception e) {
+            err = GenericUtils.accumulateException(err, e);
+        }
+
+        if (err != null) {
+            throw GenericUtils.toRuntimeException(err);
+        }
+    }
+
+    protected void closeClientInstance(SftpClient client) throws Exception {
+        if (client.isOpen()) {
+            client.close();
+        }
+    }
+
+    protected void closeSessionInstance(Callable<Exception> closer) throws 
Exception {
+        if (closer == null) {
+            return;
+        }
+
+        Exception err;
+        try {
+            err = closer.call();
+        } catch (Exception e) {
+            err = e;
+        }
+
+        if (err != null) {
+            throw err;
+        }
+    }
+
+    @Override
+    public boolean remove(String path) throws IOException {
+        SftpClient client = getClientInstance();
+        if (log.isDebugEnabled()) {
+            log.debug("remove({})[{}]", client, path);
+        }
+        client.remove(path);
+        return true;
+    }
+
+    @Override
+    public boolean mkdir(String directory) throws IOException {
+        SftpClient client = getClientInstance();
+        if (log.isDebugEnabled()) {
+            log.debug("mkdir({})[{}]", client, directory);
+        }
+        client.mkdir(directory);
+        return true;
+    }
+
+    @Override
+    public boolean rmdir(String directory) throws IOException {
+        SftpClient client = getClientInstance();
+        if (log.isDebugEnabled()) {
+            log.debug("rmdir({})[{}]", client, directory);
+        }
+        client.rmdir(directory);
+        return true;
+    }
+
+    @Override
+    public void rename(String pathFrom, String pathTo) throws IOException {
+        SftpClient client = getClientInstance();
+        boolean debugEnabled = log.isDebugEnabled();
+        if (exists(pathTo)) {
+            if (debugEnabled) {
+                log.debug("rename({})[{} => {}] target exists - attempting to 
remove", client, pathFrom, pathTo);
+            }
+            remove(pathTo);
+        }
+
+        if (debugEnabled) {
+            log.debug("rename({})[{} => {}] renaming", client, pathFrom, 
pathTo);
+        }
+        client.rename(pathFrom, pathTo);
+    }
+
+    @Override
+    public boolean exists(String path) throws IOException {
+        SftpClient client = getClientInstance();
+        try {
+            Attributes attrs = client.lstat(path);
+            return attrs != null;
+        } catch (SftpException e) {
+            if (log.isDebugEnabled()) {
+                log.debug("exists({})[{}]: {} - {}", client, path, 
e.getStatus(), e.getMessage());
+            }
+            return false;
+        }
+    }
+
+    @Override
+    public String[] listNames(String path) throws IOException {
+        DirEntry[] entries = list(path);
+        if (GenericUtils.isEmpty(entries)) {
+            return GenericUtils.EMPTY_STRING_ARRAY;
+        }
+
+        Collection<String> names = new LinkedList<>();
+        for (int index = 0; index < entries.length; index++) {
+            DirEntry de = entries[index];
+            Attributes attrs = de.getAttributes();
+            if (!attrs.isRegularFile()) {
+                continue;
+            }
+            if (attrs.isSymbolicLink()) {
+                continue;
+            }
+
+            String n = de.getFilename();
+            if (".".equals(n) || "..".equals(n)) {
+                continue;
+            }
+
+            names.add(n);
+        }
+
+        if (GenericUtils.isEmpty(names)) {
+            return GenericUtils.EMPTY_STRING_ARRAY;
+        }
+
+        return names.toArray(new String[names.size()]);
+    }
+
+    @Override
+    public DirEntry[] list(String path) throws IOException {
+        SftpClient client = getClientInstance();
+        Iterable<DirEntry> entries = client.readDir(path);
+        Collection<DirEntry> result = 
GenericUtils.stream(entries).collect(Collectors.toCollection(LinkedList::new));
+        if (GenericUtils.isEmpty(result)) {
+            return SftpClient.EMPTY_DIR_ENTRIES;
+        }
+
+        return result.toArray(new DirEntry[result.size()]);
+    }
+
+    @Override
+    public void read(String source, OutputStream outputStream) throws 
IOException {
+        SftpClient client = getClientInstance();
+        try (InputStream inputStream = client.read(source)) {
+            FileCopyUtils.copy(inputStream, outputStream);
+        }
+    }
+
+    @Override
+    public void write(InputStream inputStream, String destination) throws 
IOException {
+        SftpClient client = getClientInstance();
+        try (OutputStream outputStream = client.write(destination)) {
+            FileCopyUtils.copy(inputStream, outputStream);
+        }
+    }
+
+    @Override
+    public void append(InputStream inputStream, String destination) throws 
IOException {
+        SftpClient client = getClientInstance();
+        try (OutputStream outputStream = client.write(destination, 
OpenMode.Append)) {
+            FileCopyUtils.copy(inputStream, outputStream);
+        }
+    }
+
+    @Override
+    public InputStream readRaw(String source) throws IOException {
+        SftpClient client = getClientInstance();
+        return client.read(source);
+    }
+
+    @Override
+    public boolean finalizeRaw() throws IOException {
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "[" + getClientInstance() + "]";
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c9f8995c/sshd-spring-sftp/src/test/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactoryTest.java
----------------------------------------------------------------------
diff --git 
a/sshd-spring-sftp/src/test/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactoryTest.java
 
b/sshd-spring-sftp/src/test/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactoryTest.java
new file mode 100644
index 0000000..d6dafee
--- /dev/null
+++ 
b/sshd-spring-sftp/src/test/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactoryTest.java
@@ -0,0 +1,400 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.sshd.spring.integration.sftp;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import com.jcraft.jsch.ChannelSftp.LsEntry;
+import com.jcraft.jsch.SftpATTRS;
+
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient.Attributes;
+import org.apache.sshd.common.file.FileSystemFactory;
+import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.server.SshServer;
+import org.apache.sshd.server.scp.ScpCommandFactory;
+import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.apache.sshd.util.test.JSchLogger;
+import org.apache.sshd.util.test.Utils;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+import org.springframework.integration.file.remote.session.Session;
+import org.springframework.integration.file.remote.session.SessionFactory;
+import org.springframework.integration.sftp.session.DefaultSftpSessionFactory;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:d...@mina.apache.org";>Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class ApacheSshdSftpSessionFactoryTest extends BaseTestSupport {
+    private static final Comparator<LsEntry> BY_CASE_INSENSITIVE_FILENAME = 
new Comparator<LsEntry>() {
+        @Override
+        public int compare(LsEntry o1, LsEntry o2) {
+            if (o1 == o2) {
+                return 0;
+            } else if (o1 == null) {
+                return 1;
+            } else if (o2 == null) {
+                return -1;
+            } else {
+                return GenericUtils.safeCompare(o1.getFilename(), 
o2.getFilename(), false);
+            }
+        }
+    };
+
+    private static final Comparator<Path> BY_CASE_INSENSITIVE_FILE_PART = new 
Comparator<Path>() {
+        @Override
+        public int compare(Path o1, Path o2) {
+            String n1 = (o1 == null) ? null : 
Objects.toString(o1.getFileName(), null);
+            String n2 = (o2 == null) ? null : 
Objects.toString(o2.getFileName(), null);
+            return GenericUtils.safeCompare(n1, n2, false);
+        }
+    };
+
+    private static final Predicate<String> SYNTHETIC_DIR_ENTRY_NAME = n -> 
".".equals(n) || "..".equals(n);
+
+    private static SshServer sshd;
+    private static int port;
+    private static SshClient client;
+
+    private final FileSystemFactory fileSystemFactory;
+
+    public ApacheSshdSftpSessionFactoryTest() {
+        Path targetPath = detectTargetFolder();
+        Path parentPath = targetPath.getParent();
+        fileSystemFactory = new VirtualFileSystemFactory(parentPath);
+    }
+
+    @BeforeClass
+    public static void setupClientAndServer() throws Exception {
+        JSchLogger.init();
+        sshd = Utils.setupTestServer(ApacheSshdSftpSessionFactoryTest.class);
+        sshd.setSubsystemFactories(Collections.singletonList(new 
SftpSubsystemFactory()));
+        sshd.setCommandFactory(new ScpCommandFactory());
+        sshd.start();
+        port = sshd.getPort();
+
+        client = Utils.setupTestClient(ApacheSshdSftpSessionFactoryTest.class);
+        client.start();
+    }
+
+    @AfterClass
+    public static void tearDownClientAndServer() throws Exception {
+        if (sshd != null) {
+            try {
+                sshd.stop(true);
+            } finally {
+                sshd = null;
+            }
+        }
+
+        if (client != null) {
+            try {
+                client.stop();
+            } finally {
+                client = null;
+            }
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        sshd.setFileSystemFactory(fileSystemFactory);   // just making sure
+    }
+
+    @Test
+    public void testOpenCloseStateReport() throws Exception {
+        SessionFactory<SftpClient.DirEntry> sshdFactory = 
getSshdSessionFactory();
+        try (Session<SftpClient.DirEntry> sshdSession = 
sshdFactory.getSession()) {
+            assertTrue("Session not reported as open", sshdSession.isOpen());
+            sshdSession.close();
+            assertFalse("Session not reported as closed", 
sshdSession.isOpen());
+        }
+    }
+
+    @Test
+    public void testSharedSessionInstance() throws Exception {
+        ApacheSshdSftpSessionFactory sshdFactory = getSshdSessionFactory(true);
+        ClientSession sessionInstance;
+        try (Session<SftpClient.DirEntry> sshdSession = 
sshdFactory.getSession()) {
+            SftpClient client = (SftpClient) sshdSession.getClientInstance();
+            sessionInstance = client.getClientSession();
+            assertSame("Mismatched factory session instance", 
sshdFactory.getSharedClientSession(), sessionInstance);
+        }
+
+        for (int index = 1; index <= Byte.SIZE; index++) {
+            try (Session<SftpClient.DirEntry> sshdSession = 
sshdFactory.getSession()) {
+                SftpClient client = (SftpClient) 
sshdSession.getClientInstance();
+                assertSame("Mismatched session #" + index + " session 
instance", sessionInstance, client.getClientSession());
+            }
+        }
+    }
+
+    @Test
+    public void testWriteRemoteFileContents() throws Exception {
+        Path targetPath = detectTargetFolder();
+        Path lclSftp = Utils.resolve(targetPath, 
SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName(), 
getCurrentTestName());
+        Path srcFile = Files.createDirectories(lclSftp).resolve("source.txt");
+        List<String> expectedLines = 
Arrays.asList(getClass().getPackage().getName(), getClass().getSimpleName(), 
getCurrentTestName());
+        Files.deleteIfExists(srcFile);
+        Files.write(srcFile, expectedLines, StandardCharsets.UTF_8);
+
+        Path dstFile = srcFile.getParent().resolve("destination.txt");
+        Path parentPath = targetPath.getParent();
+        String remoteFile = Utils.resolveRelativeRemotePath(parentPath, 
dstFile);
+        SessionFactory<LsEntry> legacyFactory = getLegacySessionFactory();
+        SessionFactory<SftpClient.DirEntry> sshdFactory = 
getSshdSessionFactory();
+        try (Session<LsEntry> legacySession = legacyFactory.getSession();
+             Session<SftpClient.DirEntry> sshdSession = 
sshdFactory.getSession()) {
+            testWriteRemoteFileContents("Legacy", legacySession, srcFile, 
dstFile, remoteFile, expectedLines);
+        }
+    }
+
+    private static void testWriteRemoteFileContents(
+            String type, Session<?> session, Path srcFile, Path dstFile, 
String remotePath, List<String> expectedLines)
+                    throws Exception {
+        Files.deleteIfExists(dstFile);
+
+        try (InputStream inputStream = Files.newInputStream(srcFile)) {
+            session.write(inputStream, remotePath);
+        }
+        assertTrue(type + ": destination file not created", 
Files.exists(dstFile));
+
+        List<String> actualLines = Files.readAllLines(dstFile, 
StandardCharsets.UTF_8);
+        assertListEquals(type, expectedLines, actualLines);
+    }
+
+    @Test
+    public void testRetrieveRemoteFileContents() throws Exception {
+        Path targetPath = detectTargetFolder();
+        Path lclSftp = Utils.resolve(targetPath, 
SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName(), 
getCurrentTestName());
+        Path lclFile = Files.createDirectories(lclSftp).resolve("source.txt");
+        List<String> expectedLines = 
Arrays.asList(getClass().getPackage().getName(), getClass().getSimpleName(), 
getCurrentTestName());
+        Files.deleteIfExists(lclFile);
+        Files.write(lclFile, expectedLines, StandardCharsets.UTF_8);
+
+        Path parentPath = targetPath.getParent();
+        String remoteFile = Utils.resolveRelativeRemotePath(parentPath, 
lclFile);
+        SessionFactory<LsEntry> legacyFactory = getLegacySessionFactory();
+        SessionFactory<SftpClient.DirEntry> sshdFactory = 
getSshdSessionFactory();
+        try (Session<LsEntry> legacySession = legacyFactory.getSession();
+             Session<SftpClient.DirEntry> sshdSession = 
sshdFactory.getSession()) {
+            for (boolean directStream : new boolean[]{true, false}) {
+                List<String> legacyLines = readRemoteFileLines(legacySession, 
remoteFile, directStream);
+                assertListEquals("Pure legacy lines - direct=" + directStream, 
expectedLines, legacyLines);
+
+                List<String> sshdLines = readRemoteFileLines(sshdSession, 
remoteFile, directStream);
+                assertListEquals("Legacy vs. SSHD lines - direct=" + 
directStream, legacyLines, sshdLines);
+            }
+        }
+    }
+
+    private static List<String> readRemoteFileLines(Session<?> session, String 
remoteFile, boolean directStream) throws Exception {
+        if (directStream) {
+            try (InputStream rawStream = session.readRaw(remoteFile)) {
+                try {
+                    return IoUtils.readAllLines(rawStream);
+                } finally {
+                    session.finalizeRaw();
+                }
+            }
+        } else {
+            try (ByteArrayOutputStream baos = new 
ByteArrayOutputStream(Byte.MAX_VALUE)) {
+                session.read(remoteFile, baos);
+
+                try (ByteArrayInputStream bais = new 
ByteArrayInputStream(baos.toByteArray())) {
+                    return IoUtils.readAllLines(bais);
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testListContents() throws Exception {
+        Path targetPath = detectTargetFolder();
+        Path lclSftp = Utils.resolve(targetPath, 
SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName(), 
getCurrentTestName());
+        Utils.deleteRecursive(lclSftp); // start clean
+
+        List<Path> subFolders = new ArrayList<>();
+        for (int index = 1; index <= Byte.SIZE; index++) {
+            Path dir = Files.createDirectories(lclSftp.resolve("dir" + index));
+            subFolders.add(dir);
+        }
+        Collections.sort(subFolders, BY_CASE_INSENSITIVE_FILE_PART);
+
+        List<Path> subFiles = new ArrayList<>();
+        for (int index = 1; index <= Byte.SIZE; index++) {
+            Path file = Files.write(lclSftp.resolve("file" + index + ".txt"),
+                (getClass().getSimpleName() + "#" + getCurrentTestName() + "-" 
+ index).getBytes(StandardCharsets.UTF_8));
+            subFiles.add(file);
+        }
+        Collections.sort(subFiles, BY_CASE_INSENSITIVE_FILE_PART);
+
+        Path parentPath = targetPath.getParent();
+        String remotePath = Utils.resolveRelativeRemotePath(parentPath, 
lclSftp);
+        SessionFactory<LsEntry> legacyFactory = getLegacySessionFactory();
+        SessionFactory<SftpClient.DirEntry> sshdFactory = 
getSshdSessionFactory();
+        try (Session<LsEntry> legacySession = legacyFactory.getSession();
+             Session<SftpClient.DirEntry> sshdSession = 
sshdFactory.getSession()) {
+            SftpClient.DirEntry[] sshdEntries = sshdSession.list(remotePath);
+            List<SftpClient.DirEntry> sshdFiles = new ArrayList<>();
+            List<SftpClient.DirEntry> sshdDirs = new ArrayList<>();
+            for (SftpClient.DirEntry de : sshdEntries) {
+                String filename = de.getFilename();
+                if (SYNTHETIC_DIR_ENTRY_NAME.test(filename)) {
+                    continue;
+                }
+
+                Attributes attrs = de.getAttributes();
+                if (attrs.isDirectory()) {
+                    sshdDirs.add(de);
+                } else if (attrs.isRegularFile()) {
+                    sshdFiles.add(de);
+                }
+            }
+
+            Collections.sort(sshdDirs, 
SftpClient.DirEntry.BY_CASE_INSENSITIVE_FILENAME);
+            assertLocalEntriesEqual(subFolders, sshdDirs, true);
+            Collections.sort(sshdFiles, 
SftpClient.DirEntry.BY_CASE_INSENSITIVE_FILENAME);
+            assertLocalEntriesEqual(subFiles, sshdFiles, false);
+
+            LsEntry[] legacyEntries = legacySession.list(remotePath);
+            List<LsEntry> legacyFiles = new ArrayList<>();
+            List<LsEntry> legacyDirs = new ArrayList<>();
+            for (LsEntry lse : legacyEntries) {
+                String filename = lse.getFilename();
+                if (SYNTHETIC_DIR_ENTRY_NAME.test(filename)) {
+                    continue;
+                }
+
+                SftpATTRS attrs = lse.getAttrs();
+                if (attrs.isDir()) {
+                    legacyDirs.add(lse);
+                } else if (attrs.isReg()) {
+                    legacyFiles.add(lse);
+                }
+            }
+
+            Collections.sort(legacyDirs, BY_CASE_INSENSITIVE_FILENAME);
+            assertRemoteEntriesEqual(legacyDirs, sshdDirs, true);
+            Collections.sort(legacyFiles, BY_CASE_INSENSITIVE_FILENAME);
+            assertRemoteEntriesEqual(legacyFiles, sshdFiles, false);
+
+            List<String> sshdNames = 
Stream.of(sshdSession.listNames(remotePath))
+                    .filter(SYNTHETIC_DIR_ENTRY_NAME.negate())
+                    .collect(Collectors.toList());
+            Collections.sort(sshdNames, String.CASE_INSENSITIVE_ORDER);
+
+            List<String> localNames = subFiles.stream()
+                    .map(Path::getFileName)
+                    .map(Objects::toString)
+                    .collect(Collectors.toList());
+            Collections.sort(localNames, String.CASE_INSENSITIVE_ORDER);
+            assertListEquals("Local names mismatch", localNames, sshdNames);
+
+            List<String> legacyNames = 
Stream.of(legacySession.listNames(remotePath))
+                    .filter(SYNTHETIC_DIR_ENTRY_NAME.negate())
+                    .collect(Collectors.toList());
+            Collections.sort(legacyNames, String.CASE_INSENSITIVE_ORDER);
+            assertListEquals("Remote names mismatch", legacyNames, sshdNames);
+        }
+    }
+
+    private static void assertLocalEntriesEqual(List<Path> expected, 
List<SftpClient.DirEntry> actual, boolean dirs) {
+        assertEquals("Mismatched dir=" + dirs + " entries count", 
expected.size(), actual.size());
+        for (int index = 0; index < expected.size(); index++) {
+            Path path = expected.get(index);
+            SftpClient.DirEntry de = actual.get(index);
+            assertEquals("Mismatched filename at dirs=" + dirs + " index=" + 
index, Objects.toString(path.getFileName(), null), de.getFilename());
+
+            Attributes deAttrs = de.getAttributes();
+            assertEquals("Mismatched SSHD directory indicator for " + path, 
dirs, deAttrs.isDirectory());
+        }
+    }
+
+    private static void assertRemoteEntriesEqual(List<LsEntry> expected, 
List<SftpClient.DirEntry> actual, boolean dirs) {
+        assertEquals("Mismatched dir=" + dirs + " entries count", 
expected.size(), actual.size());
+        for (int index = 0; index < expected.size(); index++) {
+            LsEntry lse = expected.get(index);
+            SftpClient.DirEntry de = actual.get(index);
+            assertEquals("Mismatched filename at dirs=" + dirs + " index=" + 
index, lse.getFilename(), de.getFilename());
+
+            SftpATTRS lsAttrs = lse.getAttrs();
+            Attributes deAttrs = de.getAttributes();
+            assertEquals("Mismatched legacy directory indicator for " + 
lse.getFilename(), dirs, lsAttrs.isDir());
+            assertEquals("Mismatched SSHD directory indicator for " + 
de.getFilename(), dirs, deAttrs.isDirectory());
+        }
+    }
+
+    protected SessionFactory<LsEntry> getLegacySessionFactory() {
+        DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory();
+        factory.setHost(TEST_LOCALHOST);
+        factory.setPort(port);
+        factory.setUser(getCurrentTestName());
+        factory.setPassword(getCurrentTestName());
+        factory.setTimeout(30 * 1000);
+        factory.setEnableDaemonThread(true);
+        factory.setAllowUnknownKeys(true);
+        return factory;
+    }
+
+    protected ApacheSshdSftpSessionFactory getSshdSessionFactory() throws 
Exception {
+        return getSshdSessionFactory(false);
+    }
+
+    protected ApacheSshdSftpSessionFactory getSshdSessionFactory(boolean 
sharedSession) throws Exception {
+        ApacheSshdSftpSessionFactory factory = new 
ApacheSshdSftpSessionFactory(sharedSession);
+        factory.setHost(TEST_LOCALHOST);
+        factory.setPort(port);
+        factory.setUser(getCurrentTestName());
+        factory.setPassword(getCurrentTestName());
+        factory.setSshClient(client);
+        factory.setConnectTimeout(TimeUnit.SECONDS.toMillis(7L));
+        factory.setAuthenticationTimeout(TimeUnit.SECONDS.toMillis(11L));
+        factory.afterPropertiesSet();
+        return factory;
+    }
+}

Reply via email to