[prev in list] [next in list] [prev in thread] [next in thread] 

List:       mina-commits
Subject:    mina-sshd git commit: [SSHD-108] Add upload monitoring to sftp
From:       lgoldstein () apache ! org
Date:       2015-10-22 9:06:12
Message-ID: 176cc12e23e4407bb5579e890c802844 () git ! apache ! org
[Download RAW message or body]

Repository: mina-sshd
Updated Branches:
  refs/heads/master e1b8bc40c -> 0c443af5c


[SSHD-108] Add upload monitoring to sftp


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

Branch: refs/heads/master
Commit: 0c443af5c603cbbe2fd4e65a78c0d59814fd568f
Parents: e1b8bc4
Author: Lyor Goldstein <lgoldstein@vmware.com>
Authored: Thu Oct 22 12:05:54 2015 +0300
Committer: Lyor Goldstein <lgoldstein@vmware.com>
Committed: Thu Oct 22 12:05:54 2015 +0300

----------------------------------------------------------------------
 pom.xml                                         |   2 +-
 .../sftp/AbstractSftpEventListenerManager.java  |  58 +++
 .../server/subsystem/sftp/DirectoryHandle.java  |   1 +
 .../sshd/server/subsystem/sftp/FileHandle.java  |   8 +-
 .../sshd/server/subsystem/sftp/Handle.java      |  13 +-
 .../subsystem/sftp/SftpEventListener.java       | 267 ++++++++++++++
 .../sftp/SftpEventListenerManager.java          |  48 +++
 .../server/subsystem/sftp/SftpSubsystem.java    | 221 ++++++++++--
 .../subsystem/sftp/SftpSubsystemFactory.java    |  59 +--
 .../sftp/AbstractSftpClientTestSupport.java     |   3 +-
 .../sshd/client/subsystem/sftp/SftpTest.java    | 360 ++++++++++++++++++-
 11 files changed, 961 insertions(+), 79 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/0c443af5/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index 311d803..8e504d7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -730,7 +730,7 @@
 								</module>
 								<!-- <module name="RegexpHeader" /> -->
 								<module name="FileLength">
-									<property name="max" value="3000" />
+									<property name="max" value="3072" />
 								</module>
 								<module name="FileTabCharacter">
 									<property name="eachLine" value="true" />

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/0c443af5/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/AbstractSftpEventListenerManager.java
                
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/AbstractSftpEventListenerManager.java \
b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/AbstractSftpEventListenerManager.java
 new file mode 100644
index 0000000..b6d9e28
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/AbstractSftpEventListenerManager.java
 @@ -0,0 +1,58 @@
+/*
+ * 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.server.subsystem.sftp;
+
+import java.util.Collection;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+import org.apache.sshd.common.util.EventListenerUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class AbstractSftpEventListenerManager implements \
SftpEventListenerManager { +    private final Collection<SftpEventListener> \
sftpEventListeners = new CopyOnWriteArraySet<>(); +    private final \
SftpEventListener sftpEventListenerProxy; +
+    protected AbstractSftpEventListenerManager() {
+        sftpEventListenerProxy = \
EventListenerUtils.proxyWrapper(SftpEventListener.class, getClass().getClassLoader(), \
sftpEventListeners); +    }
+
+    public Collection<SftpEventListener> getRegisteredListeners() {
+        return sftpEventListeners;
+    }
+
+    @Override
+    public SftpEventListener getSftpEventListenerProxy() {
+        return sftpEventListenerProxy;
+    }
+
+
+    @Override
+    public boolean addSftpEventListener(SftpEventListener listener) {
+        return sftpEventListeners.add(ValidateUtils.checkNotNull(listener, "No \
listener")); +    }
+
+    @Override
+    public boolean removeSftpEventListener(SftpEventListener listener) {
+        return sftpEventListeners.remove(listener);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/0c443af5/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/DirectoryHandle.java
                
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/DirectoryHandle.java \
b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/DirectoryHandle.java \
                index b64bce3..9a813ce 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/DirectoryHandle.java
                
+++ b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/DirectoryHandle.java
 @@ -88,6 +88,7 @@ public class DirectoryHandle extends Handle implements \
Iterator<Path> {  
     @Override
     public void close() throws IOException {
+        super.close();
         markDone(); // just making sure
         ds.close();
     }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/0c443af5/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/FileHandle.java
                
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/FileHandle.java \
b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/FileHandle.java index \
                056c513..bd54bdf 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/FileHandle.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/FileHandle.java
@@ -111,7 +111,7 @@ public class FileHandle extends Handle {
             channel = FileChannel.open(file, options, attributes);
         } catch (UnsupportedOperationException e) {
             channel = FileChannel.open(file, options);
-            sftpSubsystem.setAttributes(file, attrs);
+            sftpSubsystem.doSetAttributes(file, attrs);
         }
         this.fileChannel = channel;
         this.pos = 0;
@@ -170,8 +170,12 @@ public class FileHandle extends Handle {
 
     @Override
     public void close() throws IOException {
+        super.close();
+
         FileChannel channel = getFileChannel();
-        channel.close();
+        if (channel.isOpen()) {
+            channel.close();
+        }
     }
 
     public void lock(long offset, long length, int mask) throws IOException {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/0c443af5/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/Handle.java
                
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/Handle.java \
b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/Handle.java index \
                686cf5d..9253c4a 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/Handle.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/Handle.java
@@ -21,11 +21,13 @@ package org.apache.sshd.server.subsystem.sftp;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public abstract class Handle implements java.io.Closeable {
+public abstract class Handle implements java.nio.channels.Channel {
+    private final AtomicBoolean closed = new AtomicBoolean(false);
     private Path file;
 
     protected Handle(Path file) {
@@ -37,8 +39,15 @@ public abstract class Handle implements java.io.Closeable {
     }
 
     @Override
+    public boolean isOpen() {
+        return !closed.get();
+    }
+
+    @Override
     public void close() throws IOException {
-        // ignored
+        if (!closed.getAndSet(true)) {
+            return; // debug breakpoint
+        }
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/0c443af5/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpEventListener.java
                
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpEventListener.java \
b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpEventListener.java
 new file mode 100644
index 0000000..a52f820
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpEventListener.java
 @@ -0,0 +1,267 @@
+/*
+ * 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.server.subsystem.sftp;
+
+import java.nio.file.CopyOption;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.EventListener;
+import java.util.Map;
+
+import org.apache.sshd.server.session.ServerSession;
+
+/**
+ * Can be used register for SFTP events. <B>Note:</B> it does not expose
+ * the entire set of available SFTP commands and responses (e.g., no reports
+ * for initialization, extensions, parameters re-negotiation, etc...);
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface SftpEventListener extends EventListener {
+    /**
+     * Called when the SFTP protocol has been initialized
+     *
+     * @param session The {@link ServerSession} through which the request was \
handled +     * @param version The negotiated SFTP version
+     */
+    void initialized(ServerSession session, int version);
+
+    /**
+     * Called when subsystem is destroyed since it was closed
+     *
+     * @param session The associated {@link ServerSession}
+     */
+    void destroying(ServerSession session);
+
+    /**
+     * Specified file / directory has been opened
+     *
+     * @param session      The {@link ServerSession} through which the request was \
handled +     * @param remoteHandle The (opaque) assigned handle for the file / \
directory +     * @param localHandle  The associated file / directory {@link Handle}
+     */
+    void open(ServerSession session, String remoteHandle, Handle localHandle);
+
+    /**
+     * Result of reading entries from a directory - <B>Note:</B> it may be a
+     * <U>partial</U> result if the directory contains more entries than can
+     * be accommodated in the response
+     *
+     * @param session      The {@link ServerSession} through which the request was \
handled +     * @param remoteHandle The (opaque) assigned handle for the directory
+     * @param localHandle  The associated {@link DirectoryHandle}
+     * @param entries      A {@link Map} of the listed entries - key = short name,
+     *                     value = {@link Path} of the sub-entry
+     */
+    void read(ServerSession session, String remoteHandle, DirectoryHandle \
localHandle, Map<String, Path> entries); +
+    /**
+     * Result of reading from a file
+     *
+     * @param session      The {@link ServerSession} through which the request was \
handled +     * @param remoteHandle The (opaque) assigned handle for the file
+     * @param localHandle  The associated {@link FileHandle}
+     * @param offset       Offset in file from which to read
+     * @param data         Buffer holding the read data
+     * @param dataOffset   Offset of read data in buffer
+     * @param dataLen      Requested read length
+     * @param readLen      Actual read length
+     */
+    void read(ServerSession session, String remoteHandle, FileHandle localHandle,
+              long offset, byte[] data, int dataOffset, int dataLen, int readLen);
+
+    /**
+     * Result of writing to a file
+     *
+     * @param session      The {@link ServerSession} through which the request was \
handled +     * @param remoteHandle The (opaque) assigned handle for the file
+     * @param localHandle  The associated {@link FileHandle}
+     * @param offset       Offset in file to which to write
+     * @param data         Buffer holding the written data
+     * @param dataOffset   Offset of write data in buffer
+     * @param dataLen      Requested write length
+     */
+    void write(ServerSession session, String remoteHandle, FileHandle localHandle,
+               long offset, byte[] data, int dataOffset, int dataLen);
+
+    /**
+     * Called <U>prior</U> to blocking a file section
+     *
+     * @param session      The {@link ServerSession} through which the request was \
handled +     * @param remoteHandle The (opaque) assigned handle for the file
+     * @param localHandle  The associated {@link FileHandle}
+     * @param offset       Offset in file for locking
+     * @param length       Section size for locking
+     * @param mask         Lock mask flags - see {@code SSH_FXP_BLOCK} message
+     * @see #blocked(ServerSession, String, FileHandle, long, long, int, Throwable)
+     */
+    void blocking(ServerSession session, String remoteHandle, FileHandle \
localHandle, long offset, long length, int mask); +
+    /**
+     * Called <U>after</U> blocking a file section
+     *
+     * @param session      The {@link ServerSession} through which the request was \
handled +     * @param remoteHandle The (opaque) assigned handle for the file
+     * @param localHandle  The associated {@link FileHandle}
+     * @param offset       Offset in file for locking
+     * @param length       Section size for locking
+     * @param mask         Lock mask flags - see {@code SSH_FXP_BLOCK} message
+     * @param thrown       If not-{@code null} then the reason for the failure to \
execute +     */
+    void blocked(ServerSession session, String remoteHandle, FileHandle localHandle, \
long offset, long length, int mask, Throwable thrown); +
+    /**
+     * Called <U>prior</U> to un-blocking a file section
+     *
+     * @param session      The {@link ServerSession} through which the request was \
handled +     * @param remoteHandle The (opaque) assigned handle for the file
+     * @param localHandle  The associated {@link FileHandle}
+     * @param offset       Offset in file for un-locking
+     * @param length       Section size for un-locking
+     */
+    void unblocking(ServerSession session, String remoteHandle, FileHandle \
localHandle, long offset, long length); +
+    /**
+     * Called <U>prior</U> to un-blocking a file section
+     *
+     * @param session      The {@link ServerSession} through which the request was \
handled +     * @param remoteHandle The (opaque) assigned handle for the file
+     * @param localHandle  The associated {@link FileHandle}
+     * @param offset       Offset in file for un-locking
+     * @param length       Section size for un-locking
+     * @param result       If successful (i.e., <tt>thrown</tt> is {@code null}, \
then whether +     *                     section was un-blocked
+     * @param thrown       If not-{@code null} then the reason for the failure to \
execute +     */
+    void unblocked(ServerSession session, String remoteHandle, FileHandle \
localHandle, long offset, long length, Boolean result, Throwable thrown); +
+    /**
+     * Specified file / directory has been closed
+     *
+     * @param session      The {@link ServerSession} through which the request was \
handled +     * @param remoteHandle The (opaque) assigned handle for the file / \
directory +     * @param localHandle  The associated file / directory {@link Handle}
+     */
+    void close(ServerSession session, String remoteHandle, Handle localHandle);
+
+    /**
+     * Called <U>prior</U> to creating a directory
+     *
+     * @param session The {@link ServerSession} through which the request was \
handled +     * @param path    Directory {@link Path} to be created
+     * @param attrs   Requested associated attributes to set
+     * @see #created(ServerSession, Path, Map, Throwable)
+     */
+    void creating(ServerSession session, Path path, Map<String, ?> attrs);
+
+    /**
+     * Called <U>after</U> creating a directory
+     *
+     * @param session The {@link ServerSession} through which the request was \
handled +     * @param path    Directory {@link Path} to be created
+     * @param attrs   Requested associated attributes to set
+     * @param thrown  If not-{@code null} then the reason for the failure to execute
+     */
+    void created(ServerSession session, Path path, Map<String, ?> attrs, Throwable \
thrown); +
+    /**
+     * Called <U>prior</U> to renaming a file / directory
+     *
+     * @param session The {@link ServerSession} through which the request was \
handled +     * @param srcPath The source {@link Path}
+     * @param dstPath The target {@link Path}
+     * @param opts    The resolved renaming options
+     * @see #moved(ServerSession, Path, Path, Collection, Throwable)
+     */
+    void moving(ServerSession session, Path srcPath, Path dstPath, \
Collection<CopyOption> opts); +
+    /**
+     * Called <U>after</U> renaming a file / directory
+     *
+     * @param session The {@link ServerSession} through which the request was \
handled +     * @param srcPath The source {@link Path}
+     * @param dstPath The target {@link Path}
+     * @param opts    The resolved renaming options
+     * @param thrown  If not-{@code null} then the reason for the failure to execute
+     */
+    void moved(ServerSession session, Path srcPath, Path dstPath, \
Collection<CopyOption> opts, Throwable thrown); +
+    /**
+     * Called <U>prior</U> to removing a file / directory
+     *
+     * @param session The {@link ServerSession} through which the request was \
handled +     * @param path    The {@link Path} about to be removed
+     * @see #removed(ServerSession, Path, Throwable)
+     */
+    void removing(ServerSession session, Path path);
+
+    /**
+     * Called <U>after</U> a file / directory has been removed
+     *
+     * @param session The {@link ServerSession} through which the request was \
handled +     * @param path    The {@link Path} to be removed
+     * @param thrown  If not-{@code null} then the reason for the failure to execute
+     */
+    void removed(ServerSession session, Path path, Throwable thrown);
+
+    /**
+     * Called <U>prior</U> to creating a link
+     *
+     * @param session The {@link ServerSession} through which the request was \
handled +     * @param source  The source {@link Path}
+     * @param target  The target {@link Path}
+     * @param symLink {@code true} = symbolic link
+     * @see #linked(ServerSession, Path, Path, boolean, Throwable)
+     */
+    void linking(ServerSession session, Path source, Path target, boolean symLink);
+
+    /**
+     * Called <U>after</U> creating a link
+     *
+     * @param session The {@link ServerSession} through which the request was \
handled +     * @param source  The source {@link Path}
+     * @param target  The target {@link Path}
+     * @param symLink {@code true} = symbolic link
+     * @param thrown  If not-{@code null} then the reason for the failure to execute
+     */
+    void linked(ServerSession session, Path source, Path target, boolean symLink, \
Throwable thrown); +
+    /**
+     * Called <U>prior</U> to modifying the attributes of a file / directory
+     *
+     * @param session The {@link ServerSession} through which the request was \
handled +     * @param path    The file / directory {@link Path} to be modified
+     * @param attrs   The attributes {@link Map} - names and values depend on the
+     *                O/S, view, type, etc...
+     * @see #modifiedAttributes(ServerSession, Path, Map, Throwable)
+     */
+    void modifyingAttributes(ServerSession session, Path path, Map<String, ?> \
attrs); +
+    /**
+     * Called <U>after</U> modifying the attributes of a file / directory
+     *
+     * @param session The {@link ServerSession} through which the request was \
handled +     * @param path    The file / directory {@link Path} to be modified
+     * @param attrs   The attributes {@link Map} - names and values depend on the
+     *                O/S, view, type, etc...
+     * @param thrown  If not-{@code null} then the reason for the failure to execute
+     */
+    void modifiedAttributes(ServerSession session, Path path, Map<String, ?> attrs, \
Throwable thrown); +}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/0c443af5/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpEventListenerManager.java
                
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpEventListenerManager.java \
b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpEventListenerManager.java
 new file mode 100644
index 0000000..3f91033
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpEventListenerManager.java
 @@ -0,0 +1,48 @@
+/*
+ * 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.server.subsystem.sftp;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface SftpEventListenerManager {
+    /**
+     * @return An instance representing <U>all</U> the currently
+     * registered listeners. Any method invocation is <U>replicated</U>
+     * to the actually registered listeners
+     */
+    SftpEventListener getSftpEventListenerProxy();
+
+    /**
+     * Register a listener instance
+     *
+     * @param listener The {@link SftpEventListener} instance to add - never {@code \
null} +     * @return {@code true} if listener is a previously un-registered one
+     */
+    boolean addSftpEventListener(SftpEventListener listener);
+
+    /**
+     * Remove a listener instance
+     *
+     * @param listener The {@link SftpEventListener} instance to remove - never \
{@code null} +     * @return {@code true} if listener is a (removed) registered one
+     */
+    boolean removeSftpEventListener(SftpEventListener listener);
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/0c443af5/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
                
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java \
b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java \
                index eb06156..2b2e8dd 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
                
+++ b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
 @@ -61,6 +61,7 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.TreeMap;
+import java.util.concurrent.CopyOnWriteArraySet;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
 
@@ -78,6 +79,7 @@ import org.apache.sshd.common.subsystem.sftp.SftpConstants;
 import org.apache.sshd.common.subsystem.sftp.extensions.SpaceAvailableExtensionInfo;
 import org.apache.sshd.common.subsystem.sftp.extensions.openssh.AbstractOpenSSHExtensionParser.OpenSSHExtension;
  import org.apache.sshd.common.subsystem.sftp.extensions.openssh.FsyncExtensionParser;
 +import org.apache.sshd.common.util.EventListenerUtils;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.Int2IntFunction;
 import org.apache.sshd.common.util.OsUtils;
@@ -96,13 +98,16 @@ import org.apache.sshd.server.Environment;
 import org.apache.sshd.server.ExitCallback;
 import org.apache.sshd.server.SessionAware;
 import org.apache.sshd.server.session.ServerSession;
+import org.apache.sshd.server.session.ServerSessionHolder;
 
 /**
  * SFTP subsystem
  *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public class SftpSubsystem extends AbstractLoggingBean implements Command, Runnable, \
SessionAware, FileSystemAware { +public class SftpSubsystem
+        extends AbstractLoggingBean
+        implements Command, Runnable, SessionAware, FileSystemAware, \
ServerSessionHolder, SftpEventListenerManager {  
     /**
      * Properties key for the maximum of available open handles per session.
@@ -138,7 +143,7 @@ public class SftpSubsystem extends AbstractLoggingBean implements \
Command, Runna  public static final String SFTP_VERSION = "sftp-version";
 
     public static final int LOWER_SFTP_IMPL = SftpConstants.SFTP_V3; // Working \
                implementation from v3
-    public static final int HIGHER_SFTP_IMPL = SftpConstants.SFTP_V6; //  .. up to
+    public static final int HIGHER_SFTP_IMPL = SftpConstants.SFTP_V6; //  .. up to \
and including  public static final String ALL_SFTP_IMPL;
 
     /**
@@ -245,7 +250,6 @@ public class SftpSubsystem extends AbstractLoggingBean implements \
Command, Runna  protected Random randomizer;
     protected int fileHandleSize = DEFAULT_FILE_HANDLE_SIZE;
     protected int maxFileHandleRounds = DEFAULT_FILE_HANDLE_ROUNDS;
-    protected ServerSession session;
     protected boolean closed;
     protected ExecutorService executors;
     protected boolean shutdownExecutor;
@@ -257,9 +261,12 @@ public class SftpSubsystem extends AbstractLoggingBean \
implements Command, Runna  protected int version;
     protected final Map<String, byte[]> extensions = new HashMap<>();
     protected final Map<String, Handle> handles = new HashMap<>();
-
     protected final UnsupportedAttributePolicy unsupportedAttributePolicy;
 
+    private ServerSession serverSession;
+    private final Collection<SftpEventListener> sftpEventListeners = new \
CopyOnWriteArraySet<>(); +    private final SftpEventListener sftpEventListenerProxy;
+
     /**
      * @param executorService The {@link ExecutorService} to be used by
      *                        the {@link SftpSubsystem} command when starting \
execution. If @@ -280,10 +287,8 @@ public class SftpSubsystem extends \
AbstractLoggingBean implements Command, Runna  shutdownExecutor = shutdownOnExit;
         }
 
-        if (policy == null) {
-            throw new IllegalArgumentException("No policy provided");
-        }
-        unsupportedAttributePolicy = policy;
+        unsupportedAttributePolicy = ValidateUtils.checkNotNull(policy, "No policy \
provided"); +        sftpEventListenerProxy = \
EventListenerUtils.proxyWrapper(SftpEventListener.class, getClass().getClassLoader(), \
sftpEventListeners);  }
 
     public int getVersion() {
@@ -295,8 +300,23 @@ public class SftpSubsystem extends AbstractLoggingBean \
implements Command, Runna  }
 
     @Override
+    public SftpEventListener getSftpEventListenerProxy() {
+        return sftpEventListenerProxy;
+    }
+
+    @Override
+    public boolean addSftpEventListener(SftpEventListener listener) {
+        return sftpEventListeners.add(ValidateUtils.checkNotNull(listener, "No \
listener")); +    }
+
+    @Override
+    public boolean removeSftpEventListener(SftpEventListener listener) {
+        return sftpEventListeners.remove(listener);
+    }
+
+    @Override
     public void setSession(ServerSession session) {
-        this.session = session;
+        this.serverSession = session;
 
         FactoryManager manager = session.getFactoryManager();
         Factory<? extends Random> factory = manager.getRandomFactory();
@@ -316,6 +336,11 @@ public class SftpSubsystem extends AbstractLoggingBean \
implements Command, Runna  }
 
     @Override
+    public ServerSession getServerSession() {
+        return serverSession;
+    }
+
+    @Override
     public void setFileSystem(FileSystem fileSystem) {
         if (fileSystem != this.fileSystem) {
             this.fileSystem = fileSystem;
@@ -948,6 +973,7 @@ public class SftpSubsystem extends AbstractLoggingBean implements \
Command, Runna  
     protected void doVersionSelect(Buffer buffer, int id) throws IOException {
         String proposed = buffer.getString();
+        ServerSession session = getServerSession();
         /*
          * The 'version-select' MUST be the first request from the client to the
          * server; if it is not, the server MUST fail the request and close the
@@ -1030,6 +1056,7 @@ public class SftpSubsystem extends AbstractLoggingBean \
implements Command, Runna  int hig = HIGHER_SFTP_IMPL;
         String available = ALL_SFTP_IMPL;
         // check if user wants to use a specific version
+        ServerSession session = getServerSession();
         Integer sftpVersion = PropertyResolverUtils.getInteger(session, \
SFTP_VERSION);  if (sftpVersion != null) {
             int forcedValue = sftpVersion;
@@ -1043,7 +1070,7 @@ public class SftpSubsystem extends AbstractLoggingBean \
implements Command, Runna  
         if (log.isTraceEnabled()) {
             log.trace("checkVersionCompatibility(id={}) - proposed={}, \
                available={}",
-                    id, proposed, available);
+                      id, proposed, available);
         }
 
         if ((proposed < low) || (proposed > hig)) {
@@ -1078,7 +1105,16 @@ public class SftpSubsystem extends AbstractLoggingBean \
implements Command, Runna  }
 
         FileHandle fileHandle = validateHandle(handle, p, FileHandle.class);
-        fileHandle.lock(offset, length, mask);
+        SftpEventListener listener = getSftpEventListenerProxy();
+        ServerSession session = getServerSession();
+        listener.blocking(session, handle, fileHandle, offset, length, mask);
+        try {
+            fileHandle.lock(offset, length, mask);
+            listener.blocked(session, handle, fileHandle, offset, length, mask, \
null); +        } catch (IOException | RuntimeException e) {
+            listener.blocked(session, handle, fileHandle, offset, length, mask, e);
+            throw e;
+        }
     }
 
     protected void doUnblock(Buffer buffer, int id) throws IOException {
@@ -1104,7 +1140,17 @@ public class SftpSubsystem extends AbstractLoggingBean \
implements Command, Runna  }
 
         FileHandle fileHandle = validateHandle(handle, p, FileHandle.class);
-        return fileHandle.unlock(offset, length);
+        SftpEventListener listener = getSftpEventListenerProxy();
+        ServerSession session = getServerSession();
+        listener.unblocking(session, handle, fileHandle, offset, length);
+        try {
+            boolean result = fileHandle.unlock(offset, length);
+            listener.unblocked(session, handle, fileHandle, offset, length, \
Boolean.valueOf(result), null); +            return result;
+        } catch (IOException | RuntimeException e) {
+            listener.unblocked(session, handle, fileHandle, offset, length, null, \
e); +            throw e;
+        }
     }
 
     protected void doLink(Buffer buffer, int id) throws IOException {
@@ -1159,10 +1205,19 @@ public class SftpSubsystem extends AbstractLoggingBean \
implements Command, Runna  id, linkPath, link, targetPath, target, symLink);
         }
 
-        if (symLink) {
-            Files.createSymbolicLink(link, target);
-        } else {
-            Files.createLink(link, target);
+        SftpEventListener listener = getSftpEventListenerProxy();
+        ServerSession session = getServerSession();
+        listener.linking(session, link, target, symLink);
+        try {
+            if (symLink) {
+                Files.createSymbolicLink(link, target);
+            } else {
+                Files.createLink(link, target);
+            }
+            listener.linked(session, link, target, symLink, null);
+        } catch (IOException | RuntimeException e) {
+            listener.linked(session, link, target, symLink, e);
+            throw e;
         }
     }
 
@@ -1231,7 +1286,17 @@ public class SftpSubsystem extends AbstractLoggingBean \
                implements Command, Runna
     protected void doRename(int id, String oldPath, String newPath, \
Collection<CopyOption> opts) throws IOException {  Path o = resolveFile(oldPath);
         Path n = resolveFile(newPath);
-        Files.move(o, n, GenericUtils.isEmpty(opts) ? IoUtils.EMPTY_COPY_OPTIONS : \
opts.toArray(new CopyOption[opts.size()])); +        SftpEventListener listener = \
getSftpEventListenerProxy(); +        ServerSession session = getServerSession();
+
+        listener.moving(session, o, n, opts);
+        try {
+            Files.move(o, n, GenericUtils.isEmpty(opts) ? IoUtils.EMPTY_COPY_OPTIONS \
: opts.toArray(new CopyOption[opts.size()])); +            listener.moved(session, o, \
n, opts, null); +        } catch (IOException | RuntimeException e) {
+            listener.moved(session, o, n, opts, e);
+            throw e;
+        }
     }
 
     // see https://tools.ietf.org/html/draft-ietf-secsh-filexfer-extensions-00#section-7
 @@ -1511,12 +1576,32 @@ public class SftpSubsystem extends AbstractLoggingBean \
implements Command, Runna  Path p = resolveFile(path);
         log.debug("Received SSH_FXP_RMDIR (path={})[{}]", path, p);
         if (Files.isDirectory(p, options)) {
-            Files.delete(p);
+            doRemove(id, p);
         } else {
             throw new NotDirectoryException(p.toString());
         }
     }
 
+    /**
+     * Called when need to delete a file / directory - also informs the {@link \
SftpEventListener} +     *
+     * @param id Deletion request ID
+     * @param p {@link Path} to delete
+     * @throws IOException If failed to delete
+     */
+    protected void doRemove(int id, Path p) throws IOException {
+        SftpEventListener listener = getSftpEventListenerProxy();
+        ServerSession session = getServerSession();
+        listener.removing(session, p);
+        try {
+            Files.delete(p);
+            listener.removed(session, p, null);
+        } catch (IOException | RuntimeException e) {
+            listener.removed(session, p, e);
+            throw e;
+        }
+    }
+
     protected void doMakeDirectory(Buffer buffer, int id) throws IOException {
         String path = buffer.getString();
         Map<String, Object> attrs = readAttrs(buffer);
@@ -1548,8 +1633,17 @@ public class SftpSubsystem extends AbstractLoggingBean \
                implements Command, Runna
                 throw new FileNotFoundException(p.toString() + " already exists as a \
file");  }
         } else {
-            Files.createDirectory(p);
-            setAttributes(p, attrs);
+            SftpEventListener listener = getSftpEventListenerProxy();
+            ServerSession session = getServerSession();
+            listener.creating(session, p, attrs);
+            try {
+                Files.createDirectory(p);
+                doSetAttributes(p, attrs);
+                listener.created(session, p, attrs, null);
+            } catch (IOException | RuntimeException e) {
+                listener.created(session, p, attrs, e);
+                throw e;
+            }
         }
     }
 
@@ -1580,7 +1674,7 @@ public class SftpSubsystem extends AbstractLoggingBean \
implements Command, Runna  } else if (Files.isDirectory(p, options)) {
             throw new FileNotFoundException(p.toString() + " is as a folder");
         } else {
-            Files.delete(p);
+            doRemove(id, p);
         }
     }
 
@@ -1622,7 +1716,7 @@ public class SftpSubsystem extends AbstractLoggingBean \
implements Command, Runna  int lenPos = reply.wpos();
                 reply.putInt(0);
 
-                int count = doReadDir(id, dh, reply, \
PropertyResolverUtils.getIntProperty(session, MAX_PACKET_LENGTH_PROP, \
DEFAULT_MAX_PACKET_LENGTH)); +                int count = doReadDir(id, handle, dh, \
reply, PropertyResolverUtils.getIntProperty(getServerSession(), \
                MAX_PACKET_LENGTH_PROP, DEFAULT_MAX_PACKET_LENGTH));
                 BufferUtils.updateLengthPlaceholder(reply, lenPos, count);
                 if (log.isDebugEnabled()) {
                     log.debug("doReadDir({})[{}] - sent {} entries", handle, h, \
count); @@ -1677,7 +1771,10 @@ public class SftpSubsystem extends AbstractLoggingBean \
implements Command, Runna  throw new AccessDeniedException("Not readable: " + p);
         } else {
             String handle = generateFileHandle(p);
-            handles.put(handle, new DirectoryHandle(p));
+            DirectoryHandle dirHandle = new DirectoryHandle(p);
+            SftpEventListener listener = getSftpEventListenerProxy();
+            listener.open(getServerSession(), handle, dirHandle);
+            handles.put(handle, dirHandle);
             return handle;
         }
     }
@@ -1701,7 +1798,7 @@ public class SftpSubsystem extends AbstractLoggingBean \
                implements Command, Runna
             log.debug("Received SSH_FXP_FSETSTAT (handle={}[{}], attrs={})", handle, \
h, attrs);  }
 
-        setAttributes(validateHandle(handle, h, Handle.class).getFile(), attrs);
+        doSetAttributes(validateHandle(handle, h, Handle.class).getFile(), attrs);
     }
 
     protected void doSetStat(Buffer buffer, int id) throws IOException {
@@ -1720,7 +1817,7 @@ public class SftpSubsystem extends AbstractLoggingBean \
                implements Command, Runna
     protected void doSetStat(int id, String path, Map<String, ?> attrs) throws \
                IOException {
         log.debug("Received SSH_FXP_SETSTAT (path={}, attrs={})", path, attrs);
         Path p = resolveFile(path);
-        setAttributes(p, attrs);
+        doSetAttributes(p, attrs);
     }
 
     protected void doFStat(Buffer buffer, int id) throws IOException {
@@ -1812,13 +1909,16 @@ public class SftpSubsystem extends AbstractLoggingBean \
implements Command, Runna  } else {
             fh.write(data, doff, length, offset);
         }
+
+        SftpEventListener listener = getSftpEventListenerProxy();
+        listener.write(getServerSession(), handle, fh, offset, data, doff, length);
     }
 
     protected void doRead(Buffer buffer, int id) throws IOException {
         String handle = buffer.getString();
         long offset = buffer.getLong();
         int requestedLength = buffer.getInt();
-        int maxAllowed = PropertyResolverUtils.getIntProperty(session, \
MAX_PACKET_LENGTH_PROP, DEFAULT_MAX_PACKET_LENGTH); +        int maxAllowed = \
PropertyResolverUtils.getIntProperty(getServerSession(), MAX_PACKET_LENGTH_PROP, \
DEFAULT_MAX_PACKET_LENGTH);  int readLen = Math.min(requestedLength, maxAllowed);
 
         if (log.isTraceEnabled()) {
@@ -1858,10 +1958,13 @@ public class SftpSubsystem extends AbstractLoggingBean \
                implements Command, Runna
             log.debug("Received SSH_FXP_READ (handle={}[{}], offset={}, length={})",
                     handle, h, offset, length);
         }
-        ValidateUtils.checkTrue(length > 0, "Invalid read length: %d", length);
-        FileHandle fh = validateHandle(handle, h, FileHandle.class);
 
-        return fh.read(data, doff, length, offset);
+        ValidateUtils.checkTrue(length > 0L, "Invalid read length: %d", length);
+        FileHandle fh = validateHandle(handle, h, FileHandle.class);
+        int readLen = fh.read(data, doff, length, offset);
+        SftpEventListener listener = getSftpEventListenerProxy();
+        listener.read(getServerSession(), handle, fh, offset, data, doff, length, \
readLen); +        return readLen;
     }
 
     protected void doClose(Buffer buffer, int id) throws IOException {
@@ -1880,6 +1983,9 @@ public class SftpSubsystem extends AbstractLoggingBean \
implements Command, Runna  Handle h = handles.remove(handle);
         log.debug("Received SSH_FXP_CLOSE (handle={}[{}])", handle, h);
         validateHandle(handle, h, Handle.class).close();
+
+        SftpEventListener listener = getSftpEventListenerProxy();
+        listener.close(getServerSession(), handle, h);
     }
 
     protected void doOpen(Buffer buffer, int id) throws IOException {
@@ -1963,14 +2069,17 @@ public class SftpSubsystem extends AbstractLoggingBean \
                implements Command, Runna
                     path, Integer.toHexString(access), Integer.toHexString(pflags), \
attrs);  }
         int curHandleCount = handles.size();
-        int maxHandleCount = PropertyResolverUtils.getIntProperty(session, \
MAX_OPEN_HANDLES_PER_SESSION, DEFAULT_MAX_OPEN_HANDLES); +        int maxHandleCount \
= PropertyResolverUtils.getIntProperty(getServerSession(), \
MAX_OPEN_HANDLES_PER_SESSION, DEFAULT_MAX_OPEN_HANDLES);  if (curHandleCount > \
                maxHandleCount) {
             throw new IllegalStateException("Too many open handles: current=" + \
curHandleCount + ", max.=" + maxHandleCount);  }
 
         Path file = resolveFile(path);
         String handle = generateFileHandle(file);
-        handles.put(handle, new FileHandle(this, file, pflags, access, attrs));
+        FileHandle fileHandle = new FileHandle(this, file, pflags, access, attrs);
+        SftpEventListener listener = getSftpEventListenerProxy();
+        listener.open(getServerSession(), handle, fileHandle);
+        handles.put(handle, fileHandle);
         return handle;
     }
 
@@ -2005,6 +2114,7 @@ public class SftpSubsystem extends AbstractLoggingBean \
implements Command, Runna  if (GenericUtils.isEmpty(all)) { // i.e. validation failed
             return;
         }
+
         version = id;
         while (buffer.available() > 0) {
             String name = buffer.getString();
@@ -2018,6 +2128,9 @@ public class SftpSubsystem extends AbstractLoggingBean \
implements Command, Runna  buffer.putInt(version);
         appendExtensions(buffer, all);
 
+        SftpEventListener listener = getSftpEventListenerProxy();
+        listener.initialized(getServerSession(), version);
+
         send(buffer);
     }
 
@@ -2054,7 +2167,7 @@ public class SftpSubsystem extends AbstractLoggingBean \
implements Command, Runna  }
 
     protected List<OpenSSHExtension> resolveOpenSSHExtensions() {
-        String value = PropertyResolverUtils.getString(session, \
OPENSSH_EXTENSIONS_PROP); +        String value = \
PropertyResolverUtils.getString(getServerSession(), OPENSSH_EXTENSIONS_PROP);  if \
(value == null) {    // No override  return DEFAULT_OPEN_SSH_EXTENSIONS;
         }
@@ -2083,7 +2196,7 @@ public class SftpSubsystem extends AbstractLoggingBean \
implements Command, Runna  }
 
     protected Collection<String> getSupportedClientExtensions() {
-        String value = PropertyResolverUtils.getString(session, \
CLIENT_EXTENSIONS_PROP); +        String value = \
PropertyResolverUtils.getString(getServerSession(), CLIENT_EXTENSIONS_PROP);  if \
(value == null) {  return DEFAULT_SUPPORTED_CLIENT_EXTENSIONS;
         }
@@ -2288,36 +2401,42 @@ public class SftpSubsystem extends AbstractLoggingBean \
implements Command, Runna  
     /**
      * @param id      Request id
+     * @param handle  The (opaque) handle assigned to this directory
      * @param dir     The {@link DirectoryHandle}
      * @param buffer  The {@link Buffer} to write the results
      * @param maxSize Max. buffer size
      * @return Number of written entries
      * @throws IOException If failed to generate an entry
      */
-    protected int doReadDir(int id, DirectoryHandle dir, Buffer buffer, int maxSize) \
throws IOException { +    protected int doReadDir(int id, String handle, \
DirectoryHandle dir, Buffer buffer, int maxSize) throws IOException {  int nb = 0;
         LinkOption[] options = IoUtils.getLinkOptions(false);
+        Map<String, Path> entries = new TreeMap<>();
         while ((dir.isSendDot() || dir.isSendDotDot() || dir.hasNext()) && \
(buffer.wpos() < maxSize)) {  if (dir.isSendDot()) {
-                writeDirEntry(id, dir, buffer, nb, dir.getFile(), ".", options);
+                writeDirEntry(id, dir, entries, buffer, nb, dir.getFile(), ".", \
options);  dir.markDotSent();    // do not send it again
             } else if (dir.isSendDotDot()) {
-                writeDirEntry(id, dir, buffer, nb, dir.getFile().getParent(), "..", \
options); +                writeDirEntry(id, dir, entries, buffer, nb, \
dir.getFile().getParent(), "..", options);  dir.markDotDotSent(); // do not send it \
again  } else {
                 Path f = dir.next();
-                writeDirEntry(id, dir, buffer, nb, f, getShortName(f), options);
+                writeDirEntry(id, dir, entries, buffer, nb, f, getShortName(f), \
options);  }
 
             nb++;
         }
 
+        SftpEventListener listener = getSftpEventListenerProxy();
+        listener.read(getServerSession(), handle, dir, entries);
         return nb;
     }
 
     /**
      * @param id        Request id
      * @param dir       The {@link DirectoryHandle}
+     * @param entries   An in / out {@link Map} for updating the written entry -
+     *                  key = short name, value = entry {@link Path}
      * @param buffer    The {@link Buffer} to write the results
      * @param index     Zero-based index of the entry to be written
      * @param f         The entry {@link Path}
@@ -2325,8 +2444,10 @@ public class SftpSubsystem extends AbstractLoggingBean \
                implements Command, Runna
      * @param options   The {@link LinkOption}s to use for querying the entry-s \
                attributes
      * @throws IOException If failed to generate the entry data
      */
-    protected void writeDirEntry(int id, DirectoryHandle dir, Buffer buffer, int \
index, Path f, String shortName, LinkOption... options) throws IOException { +    \
protected void writeDirEntry(int id, DirectoryHandle dir, Map<String, Path> entries, \
Buffer buffer, int index, Path f, String shortName, LinkOption... options) +          \
                throws IOException {
         Map<String, ?> attrs = resolveFileAttributes(f, \
SftpConstants.SSH_FILEXFER_ATTR_ALL, options); +        entries.put(shortName, f);
 
         buffer.putString(shortName);
         if (version == SftpConstants.SFTP_V3) {
@@ -2634,7 +2755,20 @@ public class SftpSubsystem extends AbstractLoggingBean \
implements Command, Runna  return Collections.emptyMap();
     }
 
-    protected void setAttributes(Path file, Map<String, ?> attributes) throws \
IOException { +    protected void doSetAttributes(Path file, Map<String, ?> \
attributes) throws IOException { +        SftpEventListener listener = \
getSftpEventListenerProxy(); +        ServerSession session = getServerSession();
+        listener.modifyingAttributes(session, file, attributes);
+        try {
+            setFileAttributes(file, attributes);
+            listener.modifiedAttributes(session, file, attributes, null);
+        } catch (IOException | RuntimeException e) {
+            listener.modifiedAttributes(session, file, attributes, e);
+            throw e;
+        }
+    }
+
+    protected void setFileAttributes(Path file, Map<String, ?> attributes) throws \
IOException {  Set<String> unsupported = new HashSet<>();
         for (String attribute : attributes.keySet()) {
             String view = null;
@@ -2765,6 +2899,8 @@ public class SftpSubsystem extends AbstractLoggingBean \
implements Command, Runna  }
 
     /**
+     * Makes sure that the local handle is not null and of the specified type
+     *
      * @param <H>    The generic handle type
      * @param handle The original handle id
      * @param h      The resolved {@link Handle} instance
@@ -2824,6 +2960,13 @@ public class SftpSubsystem extends AbstractLoggingBean \
implements Command, Runna  
             closed = true;
 
+            try {
+                SftpEventListener listener = getSftpEventListenerProxy();
+                listener.destroying(getServerSession());
+            } catch (Exception e) {
+                log.warn("Failed (" + e.getClass().getSimpleName() + ") to announce \
destruction event: " + e.getMessage(), e); +            }
+
             // if thread has not completed, cancel it
             if ((pendingFuture != null) && (!pendingFuture.isDone())) {
                 boolean result = pendingFuture.cancel(true);

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/0c443af5/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystemFactory.java
                
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystemFactory.java \
b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystemFactory.java
 index ce235bf..bdc5d2e 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystemFactory.java
                
+++ b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystemFactory.java
 @@ -19,10 +19,13 @@
 
 package org.apache.sshd.server.subsystem.sftp;
 
+import java.util.Collection;
 import java.util.concurrent.ExecutorService;
 
 import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ObjectBuilder;
+import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.threads.ExecutorServiceConfigurer;
 import org.apache.sshd.server.Command;
 import org.apache.sshd.server.subsystem.SubsystemFactory;
@@ -30,36 +33,49 @@ import org.apache.sshd.server.subsystem.SubsystemFactory;
 /**
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public class SftpSubsystemFactory implements SubsystemFactory, Cloneable, \
ExecutorServiceConfigurer { +public class SftpSubsystemFactory extends \
AbstractSftpEventListenerManager implements SubsystemFactory, \
ExecutorServiceConfigurer, SftpEventListenerManager {  public static final String \
                NAME = SftpConstants.SFTP_SUBSYSTEM_NAME;
     public static final UnsupportedAttributePolicy DEFAULT_POLICY = \
UnsupportedAttributePolicy.Warn;  
-    public static class Builder implements ObjectBuilder<SftpSubsystemFactory> {
-        private final SftpSubsystemFactory factory = new SftpSubsystemFactory();
+    public static class Builder extends AbstractSftpEventListenerManager implements \
ObjectBuilder<SftpSubsystemFactory> { +        private ExecutorService executors;
+        private boolean shutdownExecutor;
+        private UnsupportedAttributePolicy policy = DEFAULT_POLICY;
 
         public Builder() {
             super();
         }
 
         public Builder withExecutorService(ExecutorService service) {
-            factory.setExecutorService(service);
+            executors = service;
             return this;
         }
 
         public Builder withShutdownOnExit(boolean shutdown) {
-            factory.setShutdownOnExit(shutdown);
+            shutdownExecutor = shutdown;
             return this;
         }
 
         public Builder withUnsupportedAttributePolicy(UnsupportedAttributePolicy p) \
                {
-            factory.setUnsupportedAttributePolicy(p);
+            policy = ValidateUtils.checkNotNull(p, "No policy");
             return this;
         }
 
         @Override
         public SftpSubsystemFactory build() {
-            // return a clone so that each invocation returns a different instance - \
                avoid shared instances
-            return factory.clone();
+            SftpSubsystemFactory factory = new SftpSubsystemFactory();
+            factory.setExecutorService(executors);
+            factory.setShutdownOnExit(shutdownExecutor);
+            factory.setUnsupportedAttributePolicy(policy);
+
+            Collection<? extends SftpEventListener> listeners = \
getRegisteredListeners(); +            if (GenericUtils.size(listeners) > 0) {
+                for (SftpEventListener l : listeners) {
+                    factory.addSftpEventListener(l);
+                }
+            }
+
+            return factory;
         }
     }
 
@@ -111,29 +127,22 @@ public class SftpSubsystemFactory implements SubsystemFactory, \
Cloneable, Execut  
     /**
      * @param p The {@link UnsupportedAttributePolicy} to use if failed to access
-     *          some local file attributes
+     *          some local file attributes - never {@code null}
      */
     public void setUnsupportedAttributePolicy(UnsupportedAttributePolicy p) {
-        if (p == null) {
-            throw new IllegalArgumentException("No policy provided");
-        }
-
-        policy = p;
+        policy = ValidateUtils.checkNotNull(p, "No policy");
     }
 
     @Override
     public Command create() {
-        return new SftpSubsystem(getExecutorService(), isShutdownOnExit(), \
                getUnsupportedAttributePolicy());
-    }
-
-    @Override
-    public SftpSubsystemFactory clone() {
-        try {
-            return getClass().cast(super.clone());  // shallow clone is good enough
-        } catch (CloneNotSupportedException e) {
-            throw new UnsupportedOperationException("Unexpected clone exception", \
e);   // unexpected since we implement cloneable +        SftpSubsystem subsystem = \
new SftpSubsystem(getExecutorService(), isShutdownOnExit(), \
getUnsupportedAttributePolicy()); +        Collection<? extends SftpEventListener> \
listeners = getRegisteredListeners(); +        if (GenericUtils.size(listeners) > 0) \
{ +            for (SftpEventListener l : listeners) {
+                subsystem.addSftpEventListener(l);
+            }
         }
-    }
-
 
+        return subsystem;
+    }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/0c443af5/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClientTestSupport.java
                
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClientTestSupport.java \
b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClientTestSupport.java
 index 2833fb9..c743cad 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClientTestSupport.java
                
+++ b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClientTestSupport.java
 @@ -22,7 +22,6 @@ package org.apache.sshd.client.subsystem.sftp;
 import java.io.IOException;
 import java.nio.file.FileSystem;
 import java.nio.file.Path;
-import java.util.Arrays;
 import java.util.Collections;
 
 import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtension;
@@ -59,7 +58,7 @@ public abstract class AbstractSftpClientTestSupport extends \
BaseTestSupport {  
     protected void setupServer() throws Exception {
         sshd = setupTestServer();
-        sshd.setSubsystemFactories(Arrays.<NamedFactory<Command>>asList(new \
SftpSubsystemFactory())); +        \
sshd.setSubsystemFactories(Collections.<NamedFactory<Command>>singletonList(new \
SftpSubsystemFactory()));  sshd.setCommandFactory(new ScpCommandFactory());
         sshd.setFileSystemFactory(fileSystemFactory);
         sshd.start();

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/0c443af5/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java
                
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java \
b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java index \
                96dc836..9a46f8e 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java
@@ -18,11 +18,6 @@
  */
 package org.apache.sshd.client.subsystem.sftp;
 
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FX_FILE_ALREADY_EXISTS;
                
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FX_NO_SUCH_FILE;
                
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.S_IRUSR;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.S_IWUSR;
-
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
@@ -31,6 +26,7 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.URI;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.CopyOption;
 import java.nio.file.FileSystem;
 import java.nio.file.Files;
 import java.nio.file.LinkOption;
@@ -46,6 +42,8 @@ import java.util.Set;
 import java.util.Vector;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
 
 import org.apache.sshd.client.SshClient;
 import org.apache.sshd.client.session.ClientSession;
@@ -55,6 +53,7 @@ import \
org.apache.sshd.client.subsystem.sftp.extensions.BuiltinSftpClientExtensi  import \
org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtension;  import \
org.apache.sshd.common.Factory;  import org.apache.sshd.common.FactoryManager;
+import org.apache.sshd.common.NamedFactory;
 import org.apache.sshd.common.PropertyResolverUtils;
 import org.apache.sshd.common.file.FileSystemFactory;
 import org.apache.sshd.common.random.Random;
@@ -66,9 +65,17 @@ import \
org.apache.sshd.common.subsystem.sftp.extensions.SupportedParser.Supporte  import \
org.apache.sshd.common.subsystem.sftp.extensions.openssh.AbstractOpenSSHExtensionParser.OpenSSHExtension;
  import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.OsUtils;
+import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
 import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.session.ServerSession;
+import org.apache.sshd.server.subsystem.sftp.DirectoryHandle;
+import org.apache.sshd.server.subsystem.sftp.FileHandle;
+import org.apache.sshd.server.subsystem.sftp.Handle;
+import org.apache.sshd.server.subsystem.sftp.SftpEventListener;
 import org.apache.sshd.server.subsystem.sftp.SftpSubsystem;
+import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
 import org.apache.sshd.util.test.JSchLogger;
 import org.apache.sshd.util.test.SimpleUserInfo;
 import org.apache.sshd.util.test.Utils;
@@ -79,6 +86,8 @@ import org.junit.FixMethodOrder;
 import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runners.MethodSorters;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import com.jcraft.jsch.ChannelSftp;
 import com.jcraft.jsch.JSch;
@@ -390,7 +399,7 @@ public class SftpTest extends AbstractSftpClientTestSupport {
 
                     // NOTE: on Windows files are always readable
                     int perms = sftp.stat(file).perms;
-                    int permsMask = S_IWUSR | (isWindows ? 0 : S_IRUSR);
+                    int permsMask = SftpConstants.S_IWUSR | (isWindows ? 0 : \
                SftpConstants.S_IRUSR);
                     assertEquals("Mismatched permissions for " + file + ": 0x" + \
Integer.toHexString(perms), 0, (perms & permsMask));  
                     javaFile.setWritable(true, false);
@@ -443,6 +452,177 @@ public class SftpTest extends AbstractSftpClientTestSupport {
 
     @Test
     public void testClient() throws Exception {
+        List<NamedFactory<Command>> factories = sshd.getSubsystemFactories();
+        assertEquals("Mismatched subsystem factories count", 1, \
GenericUtils.size(factories)); +
+        NamedFactory<Command> f = factories.get(0);
+        assertObjectInstanceOf("Not an SFTP subsystem factory", \
SftpSubsystemFactory.class, f); +
+        SftpSubsystemFactory factory = (SftpSubsystemFactory) f;
+        final AtomicInteger versionHolder = new AtomicInteger(-1);
+        final AtomicInteger openCount = new AtomicInteger(0);
+        final AtomicInteger closeCount = new AtomicInteger(0);
+        final AtomicLong readSize = new AtomicLong(0L);
+        final AtomicLong writeSize = new AtomicLong(0L);
+        final AtomicInteger entriesCount = new AtomicInteger(0);
+        final AtomicInteger creatingCount = new AtomicInteger(0);
+        final AtomicInteger createdCount = new AtomicInteger(0);
+        final AtomicInteger removingCount = new AtomicInteger(0);
+        final AtomicInteger removedCount = new AtomicInteger(0);
+        final AtomicInteger modifyingCount = new AtomicInteger(0);
+        final AtomicInteger modifiedCount = new AtomicInteger(0);
+        factory.addSftpEventListener(new SftpEventListener() {
+            private final Logger log = \
LoggerFactory.getLogger(SftpEventListener.class); +
+            @Override
+            public void initialized(ServerSession session, int version) {
+                log.info("initialized(" + session + ") version: " + version);
+                assertTrue("Initialized version below minimum", version >= \
SftpSubsystem.LOWER_SFTP_IMPL); +                assertTrue("Initialized version \
above maximum", version <= SftpSubsystem.HIGHER_SFTP_IMPL); +                \
assertTrue("Initializion re-called", versionHolder.getAndSet(version) < 0); +         \
} +
+            @Override
+            public void destroying(ServerSession session) {
+                log.info("destroying(" + session + ")");
+                assertTrue("Initialization method not called", versionHolder.get() > \
0); +            }
+
+            @Override
+            public void write(ServerSession session, String remoteHandle, FileHandle \
localHandle, long offset, byte[] data, int dataOffset, int dataLen) { +               \
writeSize.addAndGet(dataLen); +                if (log.isDebugEnabled()) {
+                    log.debug("write(" + session + ")[" + localHandle.getFile() + "] \
offset=" + offset + ", requested=" + dataLen); +                }
+            }
+
+            @Override
+            public void removing(ServerSession session, Path path) {
+                removingCount.incrementAndGet();
+                log.info("removing(" + session + ") " + path);
+            }
+
+            @Override
+            public void removed(ServerSession session, Path path, Throwable thrown) \
{ +                removedCount.incrementAndGet();
+                log.info("removed(" + session + ") " + path
+                       + ((thrown == null) ? "" : (": " + \
thrown.getClass().getSimpleName() + ": " + thrown.getMessage()))); +            }
+
+            @Override
+            public void modifyingAttributes(ServerSession session, Path path, \
Map<String, ?> attrs) { +                modifyingCount.incrementAndGet();
+                log.info("modifyingAttributes(" + session + ") " + path);
+            }
+
+            @Override
+            public void modifiedAttributes(ServerSession session, Path path, \
Map<String, ?> attrs, Throwable thrown) { +                \
modifiedCount.incrementAndGet(); +                log.info("modifiedAttributes(" + \
session + ") " + path +                       + ((thrown == null) ? "" : (": " + \
thrown.getClass().getSimpleName() + ": " + thrown.getMessage()))); +            }
+
+            @Override
+            public void read(ServerSession session, String remoteHandle, FileHandle \
localHandle, long offset, byte[] data, int dataOffset, int dataLen, int readLen) { +  \
readSize.addAndGet(readLen); +                if (log.isDebugEnabled()) {
+                    log.debug("read(" + session + ")[" + localHandle.getFile() + "] \
offset=" + offset + ", requested=" + dataLen + ", read=" + readLen); +                \
} +            }
+
+            @Override
+            public void read(ServerSession session, String remoteHandle, \
DirectoryHandle localHandle, Map<String, Path> entries) { +                int \
numEntries = GenericUtils.size(entries); +                \
entriesCount.addAndGet(numEntries); +
+                if (log.isDebugEnabled()) {
+                    log.debug("read(" + session + ")[" + localHandle.getFile() + "] \
" + numEntries + " entries"); +                }
+
+                if ((numEntries > 0) && log.isTraceEnabled()) {
+                    for (Map.Entry<String, Path> ee : entries.entrySet()) {
+                        log.trace("read(" + session + ")[" + localHandle.getFile() + \
"] " + ee.getKey() + " - " + ee.getValue()); +                    }
+                }
+            }
+
+            @Override
+            public void open(ServerSession session, String remoteHandle, Handle \
localHandle) { +                Path path = localHandle.getFile();
+                log.info("open(" + session + ")[" + remoteHandle + "] " + \
(Files.isDirectory(path) ? "directory" : "file") + " " + path); +                \
openCount.incrementAndGet(); +            }
+
+            @Override
+            public void moving(ServerSession session, Path srcPath, Path dstPath, \
Collection<CopyOption> opts) { +                log.info("moving(" + session + ")[" + \
opts + "]" + srcPath + " => " + dstPath); +            }
+
+            @Override
+            public void moved(ServerSession session, Path srcPath, Path dstPath, \
Collection<CopyOption> opts, Throwable thrown) { +                log.info("moved(" + \
session + ")[" + opts + "]" + srcPath + " => " + dstPath +                       + \
((thrown == null) ? "" : (": " + thrown.getClass().getSimpleName() + ": " + \
thrown.getMessage()))); +            }
+
+            @Override
+            public void linking(ServerSession session, Path src, Path target, \
boolean symLink) { +                log.info("linking(" + session + ")[" + symLink + \
"]" + src + " => " + target); +            }
+
+            @Override
+            public void linked(ServerSession session, Path src, Path target, boolean \
symLink, Throwable thrown) { +                log.info("linked(" + session + ")[" + \
symLink + "]" + src + " => " + target +                      + ((thrown == null) ? "" \
: (": " + thrown.getClass().getSimpleName() + ": " + thrown.getMessage()))); +        \
} +
+            @Override
+            public void creating(ServerSession session, Path path, Map<String, ?> \
attrs) { +                creatingCount.incrementAndGet();
+                log.info("creating(" + session + ") " + (Files.isDirectory(path) ? \
"directory" : "file") + " " + path); +            }
+
+            @Override
+            public void created(ServerSession session, Path path, Map<String, ?> \
attrs, Throwable thrown) { +                createdCount.incrementAndGet();
+                log.info("created(" + session + ") " + (Files.isDirectory(path) ? \
"directory" : "file") + " " + path +                       + ((thrown == null) ? "" : \
(": " + thrown.getClass().getSimpleName() + ": " + thrown.getMessage()))); +          \
} +
+            @Override
+            public void blocking(ServerSession session, String remoteHandle, \
FileHandle localHandle, long offset, long length, int mask) { +                \
log.info("blocking(" + session + ")[" + localHandle.getFile() + "]" +                 \
+ " offset=" + offset + ", length=" + length + ", mask=0x" + \
Integer.toHexString(mask)); +            }
+
+            @Override
+            public void blocked(ServerSession session, String remoteHandle, \
FileHandle localHandle, +                                long offset, long length, \
int mask, Throwable thrown) { +                log.info("blocked(" + session + ")[" + \
localHandle.getFile() + "]" +                       + " offset=" + offset + ", \
length=" + length + ", mask=0x" + Integer.toHexString(mask) +                       + \
((thrown == null) ? "" : (": " + thrown.getClass().getSimpleName() + ": " + \
thrown.getMessage()))); +            }
+
+            @Override
+            public void unblocking(ServerSession session, String remoteHandle, \
FileHandle localHandle, long offset, long length) { +                \
log.info("unblocking(" + session + ")[" + localHandle.getFile() + "] offset=" + \
offset + ", length=" + length); +            }
+
+            @Override
+            public void unblocked(ServerSession session, String remoteHandle, \
FileHandle localHandle, +                                  long offset, long length, \
Boolean result, Throwable thrown) { +                log.info("unblocked(" + session \
+ ")[" + localHandle.getFile() + "]" +                        + " offset=" + offset + \
", length=" + length + ", result=" + result +                        + ((thrown == \
null) ? "" : (": " + thrown.getClass().getSimpleName() + ": " + \
thrown.getMessage()))); +            }
+
+            @Override
+            public void close(ServerSession session, String remoteHandle, Handle \
localHandle) { +                Path path = localHandle.getFile();
+                log.info("close(" + session + ")[" + remoteHandle + "] " + \
(Files.isDirectory(path) ? "directory" : "file") + " " + path); +                \
closeCount.incrementAndGet(); +            }
+        });
+
         try (SshClient client = setupTestClient()) {
             client.start();
 
@@ -451,8 +631,20 @@ public class SftpTest extends AbstractSftpClientTestSupport {
                 session.auth().verify(5L, TimeUnit.SECONDS);
 
                 try (SftpClient sftp = session.createSftpClient()) {
+                    assertEquals("Mismatched negotiated version", sftp.getVersion(), \
versionHolder.get());  testClient(client, sftp);
                 }
+
+                assertEquals("Mismatched open/close count", openCount.get(), \
closeCount.get()); +                assertTrue("No entries read", entriesCount.get() \
> 0); +                assertTrue("No data read", readSize.get() > 0L);
+                assertTrue("No data written", writeSize.get() > 0L);
+                assertEquals("Mismatched removal counts", removingCount.get(), \
removedCount.get()); +                assertTrue("No removals signalled", \
removedCount.get() > 0); +                assertEquals("Mismatched creation counts", \
creatingCount.get(), createdCount.get()); +                assertTrue("No creations \
signalled", creatingCount.get() > 0); +                assertEquals("Mismatched \
modification counts", modifyingCount.get(), modifiedCount.get()); +                \
assertTrue("No modifications signalled", modifiedCount.get() > 0);  } finally {
                 client.stop();
             }
@@ -681,7 +873,7 @@ public class SftpTest extends AbstractSftpClientTestSupport {
                         sftp.rename(file2Path, file3Path);
                         fail("Unxpected rename success of " + file2Path + " => " + \
                file3Path);
                     } catch (org.apache.sshd.client.subsystem.sftp.SftpException e) \
                {
-                        assertEquals("Mismatched status for failed rename of " + \
file2Path + " => " + file3Path, SSH_FX_NO_SUCH_FILE, e.getStatus()); +                \
assertEquals("Mismatched status for failed rename of " + file2Path + " => " + \
file3Path, SftpConstants.SSH_FX_NO_SUCH_FILE, e.getStatus());  }
 
                     try (OutputStream os = sftp.write(file2Path, \
SftpClient.MIN_WRITE_BUFFER_SIZE)) { @@ -692,7 +884,7 @@ public class SftpTest \
extends AbstractSftpClientTestSupport {  sftp.rename(file1Path, file2Path);
                         fail("Unxpected rename success of " + file1Path + " => " + \
                file2Path);
                     } catch (org.apache.sshd.client.subsystem.sftp.SftpException e) \
                {
-                        assertEquals("Mismatched status for failed rename of " + \
file1Path + " => " + file2Path, SSH_FX_FILE_ALREADY_EXISTS, e.getStatus()); +         \
assertEquals("Mismatched status for failed rename of " + file1Path + " => " + \
file2Path, SftpConstants.SSH_FX_FILE_ALREADY_EXISTS, e.getStatus());  }
 
                     sftp.rename(file1Path, file2Path, \
SftpClient.CopyMode.Overwrite); @@ -918,6 +1110,126 @@ public class SftpTest extends \
AbstractSftpClientTestSupport {  public void testCreateSymbolicLink() throws \
                Exception {
         // Do not execute on windows as the file system does not support symlinks
         Assume.assumeTrue("Skip non-Unix O/S", OsUtils.isUNIX());
+        List<NamedFactory<Command>> factories = sshd.getSubsystemFactories();
+        assertEquals("Mismatched subsystem factories count", 1, \
GenericUtils.size(factories)); +
+        NamedFactory<Command> f = factories.get(0);
+        assertObjectInstanceOf("Not an SFTP subsystem factory", \
SftpSubsystemFactory.class, f); +
+        SftpSubsystemFactory factory = (SftpSubsystemFactory) f;
+        final AtomicReference<LinkData> linkDataHolder = new AtomicReference<>();
+        factory.addSftpEventListener(new SftpEventListener() {
+            @Override
+            public void write(ServerSession session, String remoteHandle, FileHandle \
localHandle, long offset, byte[] data, int dataOffset, int dataLen) { +               \
// ignored +            }
+
+            @Override
+            public void removing(ServerSession session, Path path) {
+                // ignored
+            }
+
+            @Override
+            public void removed(ServerSession session, Path path, Throwable thrown) \
{ +                // ignored
+            }
+
+            @Override
+            public void read(ServerSession session, String remoteHandle, FileHandle \
localHandle, long offset, byte[] data, int dataOffset, int dataLen, int readLen) { +  \
// ignored +            }
+
+            @Override
+            public void read(ServerSession session, String remoteHandle, \
DirectoryHandle localHandle, Map<String, Path> entries) { +                // ignored
+            }
+
+            @Override
+            public void open(ServerSession session, String remoteHandle, Handle \
localHandle) { +                // ignored
+            }
+
+            @Override
+            public void modifyingAttributes(ServerSession session, Path path, \
Map<String, ?> attrs) { +                // ignored
+            }
+
+            @Override
+            public void modifiedAttributes(ServerSession session, Path path, \
Map<String, ?> attrs, Throwable thrown) { +                // ignored
+            }
+
+            @Override
+            public void blocking(ServerSession session, String remoteHandle, \
FileHandle localHandle, long offset, long length, int mask) { +                // \
ignored +            }
+
+            @Override
+            public void blocked(ServerSession session, String remoteHandle, \
FileHandle localHandle, long offset, long length, int mask, Throwable thrown) { +     \
// ignored +            }
+
+            @Override
+            public void unblocking(ServerSession session, String remoteHandle, \
FileHandle localHandle, long offset, long length) { +                // ignored
+            }
+
+            @Override
+            public void unblocked(ServerSession session, String remoteHandle, \
FileHandle localHandle, +                                  long offset, long length, \
Boolean result, Throwable thrown) { +                // ignored
+            }
+
+            @Override
+            public void moving(ServerSession session, Path srcPath, Path dstPath, \
Collection<CopyOption> opts) { +                // ignored
+            }
+
+            @Override
+            public void moved(ServerSession session, Path srcPath, Path dstPath, \
Collection<CopyOption> opts, Throwable thrown) { +                // ignored
+            }
+
+            @Override
+            public void linking(ServerSession session, Path src, Path target, \
boolean symLink) { +                assertNull("Multiple linking calls", \
linkDataHolder.getAndSet(new LinkData(src, target, symLink))); +            }
+
+            @Override
+            public void linked(ServerSession session, Path src, Path target, boolean \
symLink, Throwable thrown) { +                LinkData data = linkDataHolder.get();
+                assertNotNull("No previous linking call", data);
+                assertSame("Mismatched source", data.getSource(), src);
+                assertSame("Mismatched target", data.getTarget(), target);
+                assertEquals("Mismatched link type", data.isSymLink(), symLink);
+                assertNull("Unexpected failure", thrown);
+            }
+
+            @Override
+            public void initialized(ServerSession session, int version) {
+                // ignored
+            }
+
+            @Override
+            public void destroying(ServerSession session) {
+                // ignored
+            }
+
+            @Override
+            public void creating(ServerSession session, Path path, Map<String, ?> \
attrs) { +                // ignored
+            }
+
+            @Override
+            public void created(ServerSession session, Path path, Map<String, ?> \
attrs, Throwable thrown) { +                // ignored
+            }
+
+            @Override
+            public void close(ServerSession session, String remoteHandle, Handle \
localHandle) { +                // ignored
+            }
+        });
 
         Path targetPath = detectTargetFolder();
         Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, \
getClass().getSimpleName(), getCurrentTestName()); @@ -956,6 +1268,7 @@ public class \
SftpTest extends AbstractSftpClientTestSupport {  Files.delete(linkPath);
             }
             assertFalse("Target link exists before linking: " + linkPath, \
Files.exists(linkPath, options)); +
             c.symlink(remSrcPath, remLinkPath);
 
             assertTrue("Symlink not created: " + linkPath, Files.exists(linkPath, \
options)); @@ -967,6 +1280,8 @@ public class SftpTest extends \
AbstractSftpClientTestSupport {  } finally {
             c.disconnect();
         }
+
+        assertNotNull("No symlink signalled", linkDataHolder.getAndSet(null));
     }
 
     protected String readFile(String path) throws Exception {
@@ -1005,4 +1320,33 @@ public class SftpTest extends AbstractSftpClientTestSupport {
         }
         return sb.toString();
     }
+
+    static class LinkData {
+        private final Path source;
+        private final Path target;
+        private final boolean symLink;
+
+        LinkData(Path src, Path target, boolean symLink) {
+            this.source = ValidateUtils.checkNotNull(src, "No source");
+            this.target = ValidateUtils.checkNotNull(target, "No target");
+            this.symLink = symLink;
+        }
+
+        public Path getSource() {
+            return source;
+        }
+
+        public Path getTarget() {
+            return target;
+        }
+
+        public boolean isSymLink() {
+            return symLink;
+        }
+
+        @Override
+        public String toString() {
+            return (isSymLink() ? "Symbolic" : "Hard") + " " + getSource() + " => " \
+ getTarget(); +        }
+    }
 }


[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic