You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by gn...@apache.org on 2013/07/22 20:37:04 UTC
[7/9] git commit: Provide an SFTP client
Provide an SFTP client
Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/21c1cee9
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/21c1cee9
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/21c1cee9
Branch: refs/heads/master
Commit: 21c1cee99ba981dd262f7911617160de0c8c6654
Parents: 16fc6a8
Author: Guillaume Nodet <gn...@apache.org>
Authored: Mon Jul 22 16:07:29 2013 +0200
Committer: Guillaume Nodet <gn...@apache.org>
Committed: Mon Jul 22 16:07:29 2013 +0200
----------------------------------------------------------------------
.../java/org/apache/sshd/ClientSession.java | 6 +
.../java/org/apache/sshd/client/ScpClient.java | 18 +
.../java/org/apache/sshd/client/SftpClient.java | 177 +++++
.../client/channel/AbstractClientChannel.java | 2 +-
.../sshd/client/scp/DefaultScpClient.java | 19 +
.../sshd/client/session/ClientSessionImpl.java | 6 +
.../sshd/client/sftp/DefaultSftpClient.java | 703 +++++++++++++++++++
.../org/apache/sshd/common/file/SshFile.java | 26 +
.../common/file/nativefs/NativeSshFile.java | 120 ++++
.../apache/sshd/server/sftp/SftpSubsystem.java | 623 +++++-----------
.../src/test/java/org/apache/sshd/SftpTest.java | 61 ++
.../sshd/sftp/reply/SshFxpStatusReply.java | 35 +-
.../java/org/apache/sshd/sftp/SftpTest.java | 11 +
13 files changed, 1328 insertions(+), 479 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/21c1cee9/sshd-core/src/main/java/org/apache/sshd/ClientSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/ClientSession.java b/sshd-core/src/main/java/org/apache/sshd/ClientSession.java
index 6b7b1af..26fd46b 100644
--- a/sshd-core/src/main/java/org/apache/sshd/ClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/ClientSession.java
@@ -24,6 +24,7 @@ import java.util.Map;
import org.apache.sshd.client.ClientFactoryManager;
import org.apache.sshd.client.ScpClient;
+import org.apache.sshd.client.SftpClient;
import org.apache.sshd.client.channel.ChannelDirectTcpip;
import org.apache.sshd.client.channel.ChannelExec;
import org.apache.sshd.client.channel.ChannelShell;
@@ -118,6 +119,11 @@ public interface ClientSession extends Session {
ScpClient createScpClient();
/**
+ * Create an SFTP client from this session.
+ */
+ SftpClient createSftpClient() throws IOException;
+
+ /**
* Start forwarding the given local address on the client to the given address on the server.
*/
SshdSocketAddress startLocalPortForwarding(SshdSocketAddress local, SshdSocketAddress remote) throws IOException;
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/21c1cee9/sshd-core/src/main/java/org/apache/sshd/client/ScpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/ScpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/ScpClient.java
index fc6bfb8..d2bdaa8 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/ScpClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/ScpClient.java
@@ -1,3 +1,21 @@
+/*
+ * 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.client;
import java.io.IOException;
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/21c1cee9/sshd-core/src/main/java/org/apache/sshd/client/SftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/SftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/SftpClient.java
new file mode 100644
index 0000000..5150a81
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/SftpClient.java
@@ -0,0 +1,177 @@
+/*
+ * 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.client;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.EnumSet;
+
+/**
+ * @author <a href="http://mina.apache.org">Apache MINA Project</a>
+ */
+public interface SftpClient {
+
+ int S_IFMT = 0170000; // bitmask for the file type bitfields
+ int S_IFSOCK = 0140000; // socket
+ int S_IFLNK = 0120000; // symbolic link
+ int S_IFREG = 0100000; // regular file
+ int S_IFBLK = 0060000; // block device
+ int S_IFDIR = 0040000; // directory
+ int S_IFCHR = 0020000; // character device
+ int S_IFIFO = 0010000; // fifo
+ int S_ISUID = 0004000; // set UID bit
+ int S_ISGID = 0002000; // set GID bit
+ int S_ISVTX = 0001000; // sticky bit
+ int S_IRUSR = 0000400;
+ int S_IWUSR = 0000200;
+ int S_IXUSR = 0000100;
+ int S_IRGRP = 0000040;
+ int S_IWGRP = 0000020;
+ int S_IXGRP = 0000010;
+ int S_IROTH = 0000004;
+ int S_IWOTH = 0000002;
+ int S_IXOTH = 0000001;
+
+ enum OpenMode {
+ Read,
+ Write,
+ Append,
+ Create,
+ Truncate,
+ Exclusive
+ }
+
+ enum Attribute {
+ Size,
+ UidGid,
+ Perms,
+ AcModTime
+ }
+
+ public static class Handle {
+ public final String id;
+ public Handle(String id) {
+ this.id = id;
+ }
+ }
+
+ public static class Attributes {
+ public EnumSet<Attribute> flags = EnumSet.noneOf(Attribute.class);
+ public long size;
+ public int uid;
+ public int gid;
+ public int perms;
+ public int atime;
+ public int mtime;
+ public Attributes size(long size) {
+ flags.add(Attribute.Size);
+ this.size = size;
+ return this;
+ }
+ public Attributes owner(int uid, int gid) {
+ flags.add(Attribute.UidGid);
+ this.uid = uid;
+ this.gid = gid;
+ return this;
+ }
+ public Attributes perms(int perms) {
+ flags.add(Attribute.Perms);
+ this.perms = perms;
+ return this;
+ }
+ public Attributes time(int atime, int mtime) {
+ flags.add(Attribute.AcModTime);
+ this.atime = atime;
+ this.mtime = mtime;
+ return this;
+ }
+ public boolean isDirectory() {
+ return (perms & S_IFMT) == S_IFDIR;
+ }
+ public boolean isRegularFile() {
+ return (perms & S_IFMT) == S_IFREG;
+ }
+ public boolean isSymlink() {
+ return (perms & S_IFMT) == S_IFLNK;
+ }
+ }
+
+ public static class DirEntry {
+ public final String filename;
+ public final String longFilename;
+ public final Attributes attributes;
+ public DirEntry(String filename, String longFilename, Attributes attributes) {
+ this.filename = filename;
+ this.longFilename = longFilename;
+ this.attributes = attributes;
+ }
+ }
+
+ //
+ // Low level API
+ //
+
+ Handle open(String path, EnumSet<OpenMode> options) throws IOException;
+
+ void close(Handle handle) throws IOException;
+
+ void remove(String path) throws IOException;
+
+ void rename(String oldPath, String newPath) throws IOException;
+
+ int read(Handle handle, long fileOffset, byte[] dst, int dstoff, int len) throws IOException;
+
+ void write(Handle handle, long fileOffset, byte[] src, int srcoff, int len) throws IOException;
+
+ void mkdir(String path) throws IOException;
+
+ void rmdir(String path) throws IOException;
+
+ Handle openDir(String path) throws IOException;
+
+ DirEntry[] readDir(Handle handle) throws IOException;
+
+ String canonicalPath(String canonical) throws IOException;
+
+ Attributes stat(String path) throws IOException;
+
+ Attributes lstat(String path) throws IOException;
+
+ Attributes stat(Handle handle) throws IOException;
+
+ void setStat(String path, Attributes attributes) throws IOException;
+
+ void setStat(Handle handle, Attributes attributes) throws IOException;
+
+ String readLink(String path) throws IOException;
+
+ void symLink(String linkPath, String targetPath) throws IOException;
+
+ //
+ // High level API
+ //
+
+ Iterable<DirEntry> readDir(String path) throws IOException;
+
+ InputStream read(String path) throws IOException;
+
+ OutputStream write(String path) throws IOException;
+
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/21c1cee9/sshd-core/src/main/java/org/apache/sshd/client/channel/AbstractClientChannel.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/channel/AbstractClientChannel.java b/sshd-core/src/main/java/org/apache/sshd/client/channel/AbstractClientChannel.java
index 40015c3..eb2ea17 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/channel/AbstractClientChannel.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/channel/AbstractClientChannel.java
@@ -127,7 +127,7 @@ public abstract class AbstractClientChannel extends AbstractChannel implements C
@Override
protected void doClose() {
super.doClose();
- IoUtils.closeQuietly(invertedIn, in, out, err);
+ IoUtils.closeQuietly(invertedIn, invertedOut, invertedErr, in, out, err);
}
public int waitFor(int mask, long timeout) {
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/21c1cee9/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java
index 2bd6985..1b590c2 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java
@@ -1,3 +1,21 @@
+/*
+ * 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.client.scp;
import java.io.IOException;
@@ -14,6 +32,7 @@ import org.apache.sshd.common.file.SshFile;
import org.apache.sshd.common.scp.ScpHelper;
/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
public class DefaultScpClient implements ScpClient {
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/21c1cee9/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
index 10825b7..d40173f 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
@@ -31,6 +31,7 @@ import org.apache.sshd.ClientSession;
import org.apache.sshd.client.ClientFactoryManager;
import org.apache.sshd.client.ScpClient;
import org.apache.sshd.client.ServerKeyVerifier;
+import org.apache.sshd.client.SftpClient;
import org.apache.sshd.client.UserAuth;
import org.apache.sshd.client.UserInteraction;
import org.apache.sshd.client.auth.UserAuthAgent;
@@ -45,6 +46,7 @@ import org.apache.sshd.client.future.AuthFuture;
import org.apache.sshd.client.future.DefaultAuthFuture;
import org.apache.sshd.client.future.OpenFuture;
import org.apache.sshd.client.scp.DefaultScpClient;
+import org.apache.sshd.client.sftp.DefaultSftpClient;
import org.apache.sshd.common.Channel;
import org.apache.sshd.common.KeyExchange;
import org.apache.sshd.common.KeyPairProvider;
@@ -256,6 +258,10 @@ public class ClientSessionImpl extends AbstractSession implements ClientSession
return new DefaultScpClient(this);
}
+ public SftpClient createSftpClient() throws IOException {
+ return new DefaultSftpClient(this);
+ }
+
public SshdSocketAddress startLocalPortForwarding(SshdSocketAddress local, SshdSocketAddress remote) throws IOException {
return getTcpipForwarder().startLocalPortForwarding(local, remote);
}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/21c1cee9/sshd-core/src/main/java/org/apache/sshd/client/sftp/DefaultSftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/sftp/DefaultSftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/sftp/DefaultSftpClient.java
new file mode 100644
index 0000000..1dd6351
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/sftp/DefaultSftpClient.java
@@ -0,0 +1,703 @@
+/*
+ * 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.client.sftp;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.sshd.ClientSession;
+import org.apache.sshd.client.SftpClient;
+import org.apache.sshd.client.channel.ChannelSubsystem;
+import org.apache.sshd.common.SshException;
+import org.apache.sshd.common.util.Buffer;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class DefaultSftpClient implements SftpClient {
+
+ public static final int SSH_FXP_INIT = 1;
+ public static final int SSH_FXP_VERSION = 2;
+ public static final int SSH_FXP_OPEN = 3;
+ public static final int SSH_FXP_CLOSE = 4;
+ public static final int SSH_FXP_READ = 5;
+ public static final int SSH_FXP_WRITE = 6;
+ public static final int SSH_FXP_LSTAT = 7;
+ public static final int SSH_FXP_FSTAT = 8;
+ public static final int SSH_FXP_SETSTAT = 9;
+ public static final int SSH_FXP_FSETSTAT = 10;
+ public static final int SSH_FXP_OPENDIR = 11;
+ public static final int SSH_FXP_READDIR = 12;
+ public static final int SSH_FXP_REMOVE = 13;
+ public static final int SSH_FXP_MKDIR = 14;
+ public static final int SSH_FXP_RMDIR = 15;
+ public static final int SSH_FXP_REALPATH = 16;
+ public static final int SSH_FXP_STAT = 17;
+ public static final int SSH_FXP_RENAME = 18;
+ public static final int SSH_FXP_READLINK = 19;
+ public static final int SSH_FXP_SYMLINK = 20;
+ public static final int SSH_FXP_STATUS = 101;
+ public static final int SSH_FXP_HANDLE = 102;
+ public static final int SSH_FXP_DATA = 103;
+ public static final int SSH_FXP_NAME = 104;
+ public static final int SSH_FXP_ATTRS = 105;
+ public static final int SSH_FXP_EXTENDED = 200;
+ public static final int SSH_FXP_EXTENDED_REPLY = 201;
+
+ public static final int SSH_FX_OK = 0;
+ public static final int SSH_FX_EOF = 1;
+ public static final int SSH_FX_NO_SUCH_FILE = 2;
+ public static final int SSH_FX_PERMISSION_DENIED = 3;
+ public static final int SSH_FX_FAILURE = 4;
+ public static final int SSH_FX_BAD_MESSAGE = 5;
+ public static final int SSH_FX_NO_CONNECTION = 6;
+ public static final int SSH_FX_CONNECTION_LOST = 7;
+ public static final int SSH_FX_OP_UNSUPPORTED = 8;
+
+ public static final int SSH_FILEXFER_ATTR_SIZE = 0x00000001;
+ public static final int SSH_FILEXFER_ATTR_UIDGID = 0x00000002;
+ public static final int SSH_FILEXFER_ATTR_PERMISSIONS = 0x00000004;
+ public static final int SSH_FILEXFER_ATTR_ACMODTIME = 0x00000008; //v3 naming convention
+ public static final int SSH_FILEXFER_ATTR_EXTENDED = 0x80000000;
+
+ public static final int SSH_FXF_READ = 0x00000001;
+ public static final int SSH_FXF_WRITE = 0x00000002;
+ public static final int SSH_FXF_APPEND = 0x00000004;
+ public static final int SSH_FXF_CREAT = 0x00000008;
+ public static final int SSH_FXF_TRUNC = 0x00000010;
+ public static final int SSH_FXF_EXCL = 0x00000020;
+
+ private final ClientSession clientSession;
+ private final ChannelSubsystem channel;
+ private final Map<Integer, Buffer> messages;
+ private final AtomicInteger cmdId = new AtomicInteger(100);
+ private final Buffer receiveBuffer = new Buffer();
+
+ public DefaultSftpClient(ClientSession clientSession) throws IOException {
+ this.clientSession = clientSession;
+ this.channel = clientSession.createSubsystemChannel("sftp");
+ this.messages = new HashMap<Integer, Buffer>();
+ try {
+ this.channel.setOut(new OutputStream() {
+ @Override
+ public void write(int b) throws IOException {
+ write(new byte[] { (byte) b }, 0, 1);
+ }
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ data(b, off, len);
+ }
+ });
+ this.channel.setErr(new ByteArrayOutputStream());
+ this.channel.open().await();
+ } catch (InterruptedException e) {
+ throw (IOException) new InterruptedIOException().initCause(e);
+ }
+ init();
+ }
+
+ /**
+ * Receive binary data
+ */
+ protected int data(byte[] buf, int start, int len) throws IOException {
+ Buffer incoming = new Buffer(buf, start, len);
+ // If we already have partial data, we need to append it to the buffer and use it
+ if (receiveBuffer.available() > 0) {
+ receiveBuffer.putBuffer(incoming);
+ incoming = receiveBuffer;
+ }
+ // Process commands
+ int rpos = incoming.rpos();
+ while (receive(incoming));
+ int read = incoming.rpos() - rpos;
+ // Compact and add remaining data
+ receiveBuffer.compact();
+ if (receiveBuffer != incoming && incoming.available() > 0) {
+ receiveBuffer.putBuffer(incoming);
+ }
+ return read;
+ }
+
+ /**
+ * Read SFTP packets from buffer
+ */
+ protected boolean receive(Buffer incoming) throws IOException {
+ int rpos = incoming.rpos();
+ int wpos = incoming.wpos();
+ if (wpos - rpos > 4) {
+ int length = incoming.getInt();
+ if (length < 5) {
+ throw new IOException("Illegal sftp packet length: " + length);
+ }
+ if (wpos - rpos >= length + 4) {
+ incoming.rpos(rpos);
+ incoming.wpos(rpos + 4 + length);
+ process(incoming);
+ incoming.rpos(rpos + 4 + length);
+ incoming.wpos(wpos);
+ return true;
+ }
+ }
+ incoming.rpos(rpos);
+ return false;
+ }
+
+ /**
+ * Process an SFTP packet
+ */
+ protected void process(Buffer incoming) throws IOException {
+ Buffer buffer = new Buffer();
+ buffer.putBuffer(incoming);
+ buffer.rpos(5);
+ int id = buffer.getInt();
+ buffer.rpos(0);
+ synchronized (messages) {
+ messages.put(id, buffer);
+ messages.notifyAll();
+ }
+ }
+
+
+ protected int send(int cmd, Buffer buffer) throws IOException {
+ int id = cmdId.incrementAndGet();
+ DataOutputStream dos = new DataOutputStream(channel.getInvertedIn());
+ dos.writeInt(5 + buffer.available());
+ dos.writeByte(cmd);
+ dos.writeInt(id);
+ dos.write(buffer.array(), buffer.rpos(), buffer.available());
+ dos.flush();
+ return id;
+ }
+
+ protected Buffer receive(int id) throws IOException {
+ synchronized (messages) {
+ while (true) {
+ Buffer buffer = messages.get(id);
+ if (buffer != null) {
+ return buffer;
+ }
+ try {
+ messages.wait();
+ } catch (InterruptedException e) {
+ throw (IOException) new InterruptedIOException().initCause(e);
+ }
+ }
+ }
+ }
+
+ protected Buffer read() throws IOException {
+ DataInputStream dis = new DataInputStream(channel.getInvertedOut());
+ int length = dis.readInt();
+ if (length < 5) {
+ throw new IllegalArgumentException();
+ }
+ Buffer buffer = new Buffer(length + 4);
+ buffer.putInt(length);
+ int nb = length;
+ while (nb > 0) {
+ int l = dis.read(buffer.array(), buffer.wpos(), nb);
+ if (l < 0) {
+ throw new IllegalArgumentException();
+ }
+ buffer.wpos(buffer.wpos() + l);
+ nb -= l;
+ }
+ return buffer;
+ }
+
+ protected void init() throws IOException {
+ // Init packet
+ DataOutputStream dos = new DataOutputStream(channel.getInvertedIn());
+ dos.writeInt(5);
+ dos.writeByte(SSH_FXP_INIT);
+ dos.writeInt(3);
+ dos.flush();
+ Buffer buffer = null;
+ synchronized (messages) {
+ while (messages.isEmpty()) {
+ try {
+ messages.wait();
+ } catch (InterruptedException e) {
+ throw (IOException) new InterruptedIOException().initCause(e);
+ }
+ }
+ buffer = messages.remove(messages.keySet().iterator().next());
+
+ }
+ int length = buffer.getInt();
+ int type = buffer.getByte();
+ int id = buffer.getInt();
+ if (type == SSH_FXP_VERSION) {
+ if (id != 3) {
+ throw new SshException("Unable to use SFTP v3, server replied with version " + id);
+ }
+ } else if (type == SSH_FXP_STATUS) {
+ int substatus = buffer.getInt();
+ String msg = buffer.getString();
+ String lang = buffer.getString();
+ throw new SshException("SFTP error (" + substatus + "): " + msg);
+ } else {
+ throw new SshException("Unexpected SFTP packet received: " + type);
+ }
+ }
+
+ protected void checkStatus(Buffer buffer) throws IOException {
+ int length = buffer.getInt();
+ int type = buffer.getByte();
+ int id = buffer.getInt();
+ if (type == SSH_FXP_STATUS) {
+ int substatus = buffer.getInt();
+ String msg = buffer.getString();
+ String lang = buffer.getString();
+ if (substatus != SSH_FX_OK) {
+ throw new SshException("SFTP error (" + substatus + "): " + msg);
+ }
+ } else {
+ throw new SshException("Unexpected SFTP packet received: " + type);
+ }
+ }
+
+ protected Handle checkHandle(Buffer buffer) throws IOException {
+ int length = buffer.getInt();
+ int type = buffer.getByte();
+ int id = buffer.getInt();
+ if (type == SSH_FXP_STATUS) {
+ int substatus = buffer.getInt();
+ String msg = buffer.getString();
+ String lang = buffer.getString();
+ throw new SshException("SFTP error (" + substatus + "): " + msg);
+ } else if (type == SSH_FXP_HANDLE) {
+ String handle = buffer.getString();
+ return new Handle(handle);
+ } else {
+ throw new SshException("Unexpected SFTP packet received: " + type);
+ }
+ }
+
+ protected Attributes checkAttributes(Buffer buffer) throws IOException {
+ int length = buffer.getInt();
+ int type = buffer.getByte();
+ int id = buffer.getInt();
+ if (type == SSH_FXP_STATUS) {
+ int substatus = buffer.getInt();
+ String msg = buffer.getString();
+ String lang = buffer.getString();
+ throw new SshException("SFTP error (" + substatus + "): " + msg);
+ } else if (type == SSH_FXP_ATTRS) {
+ return readAttributes(buffer);
+ } else {
+ throw new SshException("Unexpected SFTP packet received: " + type);
+ }
+ }
+
+ protected String checkOneName(Buffer buffer) throws IOException {
+ int length = buffer.getInt();
+ int type = buffer.getByte();
+ int id = buffer.getInt();
+ if (type == SSH_FXP_STATUS) {
+ int substatus = buffer.getInt();
+ String msg = buffer.getString();
+ String lang = buffer.getString();
+ throw new SshException("SFTP error (" + substatus + "): " + msg);
+ } else if (type == SSH_FXP_NAME) {
+ int len = buffer.getInt();
+ if (len != 1) {
+ throw new SshException("SFTP error: received " + len + " names instead of 1");
+ }
+ String name = buffer.getString();
+ String longName = buffer.getString();
+ Attributes attrs = readAttributes(buffer);
+ return name;
+ } else {
+ throw new SshException("Unexpected SFTP packet received: " + type);
+ }
+ }
+
+ protected Attributes readAttributes(Buffer buffer) throws IOException {
+ Attributes attrs = new Attributes();
+ int flags = buffer.getInt();
+ if ((flags & SSH_FILEXFER_ATTR_SIZE) != 0) {
+ attrs.flags.add(Attribute.Size);
+ attrs.size = buffer.getLong();
+ }
+ if ((flags & SSH_FILEXFER_ATTR_UIDGID) != 0) {
+ attrs.flags.add(Attribute.UidGid);
+ attrs.uid = buffer.getInt();
+ attrs.gid = buffer.getInt();
+ }
+ if ((flags & SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
+ attrs.flags.add(Attribute.Perms);
+ attrs.perms = buffer.getInt();
+ }
+ if ((flags & SSH_FILEXFER_ATTR_ACMODTIME) != 0) {
+ attrs.flags.add(Attribute.AcModTime);
+ attrs.atime = buffer.getInt();
+ attrs.mtime = buffer.getInt();
+ }
+ return attrs;
+ }
+
+ protected void writeAttributes(Buffer buffer, Attributes attributes) throws IOException {
+ int flags = 0;
+ for (Attribute a : attributes.flags) {
+ switch (a) {
+ case Size: flags |= SSH_FILEXFER_ATTR_SIZE; break;
+ case UidGid: flags |= SSH_FILEXFER_ATTR_UIDGID; break;
+ case Perms: flags |= SSH_FILEXFER_ATTR_PERMISSIONS; break;
+ case AcModTime: flags |= SSH_FILEXFER_ATTR_ACMODTIME; break;
+ }
+ }
+ buffer.putInt(flags);
+ if ((flags & SSH_FILEXFER_ATTR_SIZE) != 0) {
+ buffer.putLong(attributes.size);
+ }
+ if ((flags & SSH_FILEXFER_ATTR_UIDGID) != 0) {
+ buffer.putInt(attributes.uid);
+ buffer.putInt(attributes.gid);
+ }
+ if ((flags & SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
+ buffer.putInt(attributes.perms);
+ }
+ if ((flags & SSH_FILEXFER_ATTR_ACMODTIME) != 0) {
+ buffer.putInt(attributes.atime);
+ buffer.putInt(attributes.mtime);
+ }
+ }
+
+ public Handle open(String path, EnumSet<OpenMode> options) throws IOException {
+ Buffer buffer = new Buffer();
+ buffer.putString(path);
+ int mode = 0;
+ for (OpenMode m : options) {
+ switch (m) {
+ case Read: mode |= SSH_FXF_READ; break;
+ case Write: mode |= SSH_FXF_WRITE; break;
+ case Append: mode |= SSH_FXF_APPEND; break;
+ case Create: mode |= SSH_FXF_CREAT; break;
+ case Truncate: mode |= SSH_FXF_TRUNC; break;
+ case Exclusive: mode |= SSH_FXF_EXCL; break;
+ }
+ }
+ buffer.putInt(mode);
+ return checkHandle(receive(send(SSH_FXP_OPEN, buffer)));
+ }
+
+ public void close(Handle handle) throws IOException {
+ Buffer buffer = new Buffer();
+ buffer.putString(handle.id);
+ checkStatus(receive(send(SSH_FXP_CLOSE, buffer)));
+ }
+
+ public void remove(String path) throws IOException {
+ Buffer buffer = new Buffer();
+ buffer.putString(path);
+ checkStatus(receive(send(SSH_FXP_REMOVE, buffer)));
+ }
+
+ public void rename(String oldPath, String newPath) throws IOException {
+ Buffer buffer = new Buffer();
+ buffer.putString(oldPath);
+ buffer.putString(newPath);
+ checkStatus(receive(send(SSH_FXP_RENAME, buffer)));
+ }
+
+ public int read(Handle handle, long fileOffset, byte[] dst, int dstoff, int len) throws IOException {
+ Buffer buffer = new Buffer();
+ buffer.putString(handle.id);
+ buffer.putLong(fileOffset);
+ buffer.putInt(len);
+ buffer = receive(send(SSH_FXP_READ, buffer));
+ int length = buffer.getInt();
+ int type = buffer.getByte();
+ int id = buffer.getInt();
+ if (type == SSH_FXP_STATUS) {
+ int substatus = buffer.getInt();
+ String msg = buffer.getString();
+ String lang = buffer.getString();
+ if (substatus == SSH_FX_EOF) {
+ return -1;
+ }
+ throw new SshException("SFTP error (" + substatus + "): " + msg);
+ } else if (type == SSH_FXP_DATA) {
+ len = buffer.getInt();
+ buffer.getRawBytes(dst, dstoff, len);
+ return len;
+ } else {
+ throw new SshException("Unexpected SFTP packet received: " + type);
+ }
+ }
+
+ public void write(Handle handle, long fileOffset, byte[] src, int srcoff, int len) throws IOException {
+ Buffer buffer = new Buffer();
+ buffer.putString(handle.id);
+ buffer.putLong(fileOffset);
+ buffer.putBytes(src, srcoff, len);
+ checkStatus(receive(send(SSH_FXP_WRITE, buffer)));
+ }
+
+ public void mkdir(String path) throws IOException {
+ Buffer buffer = new Buffer();
+ buffer.putString(path);
+ checkStatus(receive(send(SSH_FXP_MKDIR, buffer)));
+ }
+
+ public void rmdir(String path) throws IOException {
+ Buffer buffer = new Buffer();
+ buffer.putString(path);
+ checkStatus(receive(send(SSH_FXP_RMDIR, buffer)));
+ }
+
+ public Handle openDir(String path) throws IOException {
+ Buffer buffer = new Buffer();
+ buffer.putString(path);
+ return checkHandle(receive(send(SSH_FXP_OPENDIR, buffer)));
+ }
+
+ public DirEntry[] readDir(Handle handle) throws IOException {
+ Buffer buffer = new Buffer();
+ buffer.putString(handle.id);
+ buffer = receive(send(SSH_FXP_READDIR, buffer));
+ int length = buffer.getInt();
+ int type = buffer.getByte();
+ int id = buffer.getInt();
+ if (type == SSH_FXP_STATUS) {
+ int substatus = buffer.getInt();
+ String msg = buffer.getString();
+ String lang = buffer.getString();
+ if (substatus == SSH_FX_EOF) {
+ return null;
+ }
+ throw new SshException("SFTP error (" + substatus + "): " + msg);
+ } else if (type == SSH_FXP_NAME) {
+ int len = buffer.getInt();
+ DirEntry[] entries = new DirEntry[len];
+ for (int i = 0; i < len; i++) {
+ String name = buffer.getString();
+ String longName = buffer.getString();
+ Attributes attrs = readAttributes(buffer);
+ entries[i] = new DirEntry(name, longName, attrs);
+ }
+ return entries;
+ } else {
+ throw new SshException("Unexpected SFTP packet received: " + type);
+ }
+ }
+
+ public String canonicalPath(String path) throws IOException {
+ Buffer buffer = new Buffer();
+ buffer.putString(path);
+ return checkOneName(receive(send(SSH_FXP_REALPATH, buffer)));
+ }
+
+ public Attributes stat(String path) throws IOException {
+ Buffer buffer = new Buffer();
+ buffer.putString(path);
+ return checkAttributes(receive(send(SSH_FXP_STAT, buffer)));
+ }
+
+ public Attributes lstat(String path) throws IOException {
+ Buffer buffer = new Buffer();
+ buffer.putString(path);
+ return checkAttributes(receive(send(SSH_FXP_LSTAT, buffer)));
+ }
+
+ public Attributes stat(Handle handle) throws IOException {
+ Buffer buffer = new Buffer();
+ buffer.putString(handle.id);
+ return checkAttributes(receive(send(SSH_FXP_FSTAT, buffer)));
+ }
+
+ public void setStat(String path, Attributes attributes) throws IOException {
+ Buffer buffer = new Buffer();
+ buffer.putString(path);
+ writeAttributes(buffer, attributes);
+ checkStatus(receive(send(SSH_FXP_SETSTAT, buffer)));
+ }
+
+ public void setStat(Handle handle, Attributes attributes) throws IOException {
+ Buffer buffer = new Buffer();
+ buffer.putString(handle.id);
+ writeAttributes(buffer, attributes);
+ checkStatus(receive(send(SSH_FXP_FSETSTAT, buffer)));
+ }
+
+ public String readLink(String path) throws IOException {
+ Buffer buffer = new Buffer();
+ buffer.putString(path);
+ return checkOneName(receive(send(SSH_FXP_READLINK, buffer)));
+ }
+
+ public void symLink(String linkPath, String targetPath) throws IOException {
+ Buffer buffer = new Buffer();
+ buffer.putString(linkPath);
+ buffer.putString(targetPath);
+ checkStatus(receive(send(SSH_FXP_RENAME, buffer)));
+ }
+
+ public Iterable<DirEntry> readDir(final String path) throws IOException {
+ return new Iterable<DirEntry>() {
+ public Iterator<DirEntry> iterator() {
+ return new Iterator<DirEntry>() {
+ Handle handle;
+ DirEntry[] entries;
+ int index;
+ {
+ open();
+ load();
+ }
+ public boolean hasNext() {
+ return entries != null && index < entries.length;
+ }
+ public DirEntry next() {
+ DirEntry entry = entries[index++];
+ if (index >= entries.length) {
+ load();
+ }
+ return entry;
+ }
+ private void open() {
+ try {
+ handle = openDir(path);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ private void load() {
+ try {
+ entries = readDir(handle);
+ index = 0;
+ if (entries == null) {
+ close(handle);
+ }
+ } catch (IOException e) {
+ entries = null;
+ try {
+ close(handle);
+ } catch (IOException t) {
+ // Ignore
+ }
+ throw new RuntimeException(e);
+ }
+ }
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ };
+ }
+ };
+ }
+
+ public InputStream read(final String path) throws IOException {
+ return new InputStream() {
+ byte[] buffer = new byte[32 * 1024];
+ int index = 0;
+ int available = 0;
+ Handle handle = DefaultSftpClient.this.open(path, EnumSet.of(OpenMode.Read));
+ long offset;
+ @Override
+ public int read() throws IOException {
+ byte[] buffer = new byte[1];
+ int read = read(buffer, 0, 1);
+ if (read > 0) {
+ return buffer[0];
+ }
+ return read;
+ }
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ int idx = off;
+ while (len > 0) {
+ if (index >= available) {
+ available = DefaultSftpClient.this.read(handle, offset, buffer, 0, buffer.length);
+ if (available < 0) {
+ if (idx == off) {
+ return -1;
+ } else {
+ break;
+ }
+ }
+ offset += available;
+ index = 0;
+ }
+ if (index >= available) {
+ break;
+ }
+ int nb = Math.min(len, available - index);
+ System.arraycopy(buffer, index, b, idx, nb);
+ index += nb;
+ idx += nb;
+ len -= nb;
+ }
+ return idx - off;
+ }
+ @Override
+ public void close() throws IOException {
+ DefaultSftpClient.this.close(handle);
+ }
+ };
+ }
+
+ public OutputStream write(final String path) throws IOException {
+ return new OutputStream() {
+ byte[] buffer = new byte[32 * 1024];
+ int index = 0;
+ Handle handle = DefaultSftpClient.this.open(path, EnumSet.of(OpenMode.Write));
+ long offset;
+ @Override
+ public void write(int b) throws IOException {
+ byte[] buffer = new byte[1];
+ buffer[0] = (byte) b;
+ write(buffer, 0, 1);
+ }
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ do {
+ int nb = Math.min(len, buffer.length - index);
+ System.arraycopy(b, off, buffer, index, nb);
+ index += nb;
+ if (index == buffer.length) {
+ flush();
+ }
+ len -= nb;
+ } while (len > 0);
+ }
+ @Override
+ public void flush() throws IOException {
+ DefaultSftpClient.this.write(handle, offset, buffer, 0, index);
+ offset += index;
+ index = 0;
+ }
+ @Override
+ public void close() throws IOException {
+ DefaultSftpClient.this.close(handle);
+ }
+ };
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/21c1cee9/sshd-core/src/main/java/org/apache/sshd/common/file/SshFile.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/file/SshFile.java b/sshd-core/src/main/java/org/apache/sshd/common/file/SshFile.java
index 511fe4c..1da1748 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/file/SshFile.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/file/SshFile.java
@@ -23,6 +23,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
/**
* This is the file abstraction used by the server.
@@ -31,6 +33,21 @@ import java.util.List;
*/
public interface SshFile {
+ enum Attribute {
+ Size, // long
+ Uid, // int
+ Owner, // String
+ Gid, // int
+ Group, // String
+ IsDirectory, // boolean
+ IsRegularFile, // boolean
+ IsSymbolicLink, // boolean
+ Permissions, // int
+ CreationTime, // long
+ LastModifiedTime, // long
+ LastAccessTime // long
+ }
+
/**
* Get the full path from the base directory of the FileSystemView.
* @return a path where the path separator is '/' (even if the operating system
@@ -44,6 +61,15 @@ public interface SshFile {
*/
String getName();
+ Map<Attribute,Object> getAttributes() throws IOException;
+
+ void setAttributes(Map<Attribute, Object> attributes) throws IOException;
+
+ Object getAttribute(Attribute attribute) throws IOException;
+
+ void setAttribute(Attribute attribute, Object value) throws IOException;
+
+
/**
* Get the owner name of the file
* @return the name of the owner.
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/21c1cee9/sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeSshFile.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeSshFile.java b/sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeSshFile.java
index d2796fd..ee7cd6e 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeSshFile.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeSshFile.java
@@ -27,10 +27,22 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.lang.reflect.Method;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.attribute.GroupPrincipal;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.UserPrincipal;
+import java.nio.file.attribute.UserPrincipalLookupService;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
import java.util.StringTokenizer;
import org.apache.sshd.common.file.SshFile;
@@ -591,4 +603,112 @@ public class NativeSshFile implements SshFile {
public String toString() {
return fileName;
}
+
+ public Map<Attribute, Object> getAttributes() throws IOException {
+ Map<String, Object> a = Files.readAttributes(file.toPath(), "unix:size,uid,owner,gid,group,isDirectory,isRegularFile,isSymbolicLink,permissions,creationTime,lastModifiedTime,lastAccessTime", LinkOption.NOFOLLOW_LINKS);
+ Map<Attribute, Object> map = new HashMap<Attribute, Object>();
+ map.put(Attribute.Size, a.get("size"));
+ map.put(Attribute.Uid, a.get("uid"));
+ map.put(Attribute.Owner, ((UserPrincipal) a.get("owner")).getName());
+ map.put(Attribute.Gid, a.get("gid"));
+ map.put(Attribute.Group, ((GroupPrincipal) a.get("group")).getName());
+ map.put(Attribute.IsDirectory, a.get("isDirectory"));
+ map.put(Attribute.IsRegularFile, a.get("isRegularFile"));
+ map.put(Attribute.IsSymbolicLink, a.get("isSymbolicLink"));
+ map.put(Attribute.CreationTime, ((FileTime) a.get("creationTime")).toMillis());
+ map.put(Attribute.LastModifiedTime, ((FileTime) a.get("lastModifiedTime")).toMillis());
+ map.put(Attribute.LastAccessTime, ((FileTime) a.get("lastAccessTime")).toMillis());
+ map.put(Attribute.Permissions, fromPerms((Set<PosixFilePermission>) a.get("permissions")));
+ return map;
+ }
+
+ private int fromPerms(Set<PosixFilePermission> perms) {
+ int p = 0;
+ for (PosixFilePermission perm : perms) {
+ switch (perm) {
+ case OWNER_READ: p |= 0000400; break;
+ case OWNER_WRITE: p |= 0000200; break;
+ case OWNER_EXECUTE: p |= 0000100; break;
+ case GROUP_READ: p |= 0000040; break;
+ case GROUP_WRITE: p |= 0000020; break;
+ case GROUP_EXECUTE: p |= 0000010; break;
+ case OTHERS_READ: p |= 0000004; break;
+ case OTHERS_WRITE: p |= 0000002; break;
+ case OTHERS_EXECUTE: p |= 0000001; break;
+ }
+ }
+ return p;
+ }
+
+ public void setAttributes(Map<Attribute, Object> attributes) throws IOException {
+ for (Attribute attribute : attributes.keySet()) {
+ String name = null;
+ Object value = attributes.get(attribute);
+ switch (attribute) {
+ case Uid: name = "unix:uid"; break;
+ case Owner: name = "unix:owner"; value = toUser((String) value); break;
+ case Gid: name = "unix:gid"; break;
+ case Group: name = "unix:group"; value = toGroup((String) value); break;
+ case CreationTime: name = "unix:creationTime"; value = FileTime.fromMillis((Long) value); break;
+ case LastModifiedTime: name = "unix:lastModifiedTime"; value = FileTime.fromMillis((Long) value); break;
+ case LastAccessTime: name = "unix:lastAccessTime"; value = FileTime.fromMillis((Long) value); break;
+ case Permissions: name = "unix:permissions"; value = toPerms((Integer) value); break;
+ }
+ if (name != null && value != null) {
+ Files.setAttribute(file.toPath(), name, value, LinkOption.NOFOLLOW_LINKS);
+ }
+ }
+ }
+
+ private GroupPrincipal toGroup(String name) throws IOException {
+ UserPrincipalLookupService lookupService = file.toPath().getFileSystem().getUserPrincipalLookupService();
+ return lookupService.lookupPrincipalByGroupName(name);
+ }
+
+ private UserPrincipal toUser(String name) throws IOException {
+ UserPrincipalLookupService lookupService = file.toPath().getFileSystem().getUserPrincipalLookupService();
+ return lookupService.lookupPrincipalByName(name);
+ }
+
+ private Set<PosixFilePermission> toPerms(int perms) {
+ Set<PosixFilePermission> p = new HashSet<PosixFilePermission>();
+ if ((perms & 0000400) != 0) {
+ p.add(PosixFilePermission.OWNER_READ);
+ }
+ if ((perms & 0000200) != 0) {
+ p.add(PosixFilePermission.OWNER_WRITE);
+ }
+ if ((perms & 0000100) != 0) {
+ p.add(PosixFilePermission.OWNER_EXECUTE);
+ }
+ if ((perms & 0000040) != 0) {
+ p.add(PosixFilePermission.GROUP_READ);
+ }
+ if ((perms & 0000020) != 0) {
+ p.add(PosixFilePermission.GROUP_WRITE);
+ }
+ if ((perms & 0000010) != 0) {
+ p.add(PosixFilePermission.GROUP_EXECUTE);
+ }
+ if ((perms & 0000004) != 0) {
+ p.add(PosixFilePermission.OTHERS_READ);
+ }
+ if ((perms & 0000002) != 0) {
+ p.add(PosixFilePermission.OTHERS_WRITE);
+ }
+ if ((perms & 0000001) != 0) {
+ p.add(PosixFilePermission.OTHERS_EXECUTE);
+ }
+ return p;
+ }
+
+ public Object getAttribute(Attribute attribute) throws IOException {
+ return getAttributes().get(attribute);
+ }
+
+ public void setAttribute(Attribute attribute, Object value) throws IOException {
+ Map<Attribute, Object> map = new HashMap<Attribute, Object>();
+ map.put(attribute, value);
+ setAttributes(map);
+ }
}