1b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair/* 2b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * Copyright 2007 the original author or authors. 3b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * 4b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * Licensed under the Apache License, Version 2.0 (the "License"); 5b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * you may not use this file except in compliance with the License. 6b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * You may obtain a copy of the License at 7b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * 8b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * http://www.apache.org/licenses/LICENSE-2.0 9b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * 10b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * Unless required by applicable law or agreed to in writing, software 11b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * distributed under the License is distributed on an "AS IS" BASIS, 12b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * See the License for the specific language governing permissions and 14b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * limitations under the License. 15b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair */ 16b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairpackage org.mockftpserver.core.session; 17b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 18b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport org.apache.log4j.Logger; 19b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport org.mockftpserver.core.MockFtpServerException; 20b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport org.mockftpserver.core.command.Command; 21b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport org.mockftpserver.core.command.CommandHandler; 22b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport org.mockftpserver.core.command.CommandNames; 23b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport org.mockftpserver.core.socket.DefaultServerSocketFactory; 24b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport org.mockftpserver.core.socket.DefaultSocketFactory; 25b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport org.mockftpserver.core.socket.ServerSocketFactory; 26b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport org.mockftpserver.core.socket.SocketFactory; 27b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport org.mockftpserver.core.util.Assert; 28b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport org.mockftpserver.core.util.AssertFailedException; 29b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 30b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport java.io.BufferedReader; 31b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport java.io.ByteArrayOutputStream; 32b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport java.io.IOException; 33b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport java.io.InputStream; 34b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport java.io.InputStreamReader; 35b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport java.io.OutputStream; 36b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport java.io.PrintWriter; 37b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport java.io.Writer; 38b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport java.net.InetAddress; 39b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport java.net.ServerSocket; 40b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport java.net.Socket; 41b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport java.net.SocketTimeoutException; 42b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport java.util.ArrayList; 43b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport java.util.HashMap; 44b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport java.util.List; 45b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport java.util.Map; 46b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport java.util.Set; 47b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairimport java.util.StringTokenizer; 48b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 49b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair/** 50b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * Default implementation of the {@link Session} interface. 51b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * 52b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @author Chris Mair 53b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @version $Revision$ - $Date$ 54b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair */ 55b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismairpublic class DefaultSession implements Session { 56b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 57b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair private static final Logger LOG = Logger.getLogger(DefaultSession.class); 58b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair private static final String END_OF_LINE = "\r\n"; 59b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair protected static final int DEFAULT_CLIENT_DATA_PORT = 21; 60b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 61b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair protected SocketFactory socketFactory = new DefaultSocketFactory(); 62b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair protected ServerSocketFactory serverSocketFactory = new DefaultServerSocketFactory(); 63b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 64b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair private BufferedReader controlConnectionReader; 65b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair private Writer controlConnectionWriter; 66b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair private Socket controlSocket; 67b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair private Socket dataSocket; 68b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair ServerSocket passiveModeDataSocket; // non-private for testing 69b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair private InputStream dataInputStream; 70b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair private OutputStream dataOutputStream; 71b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair private Map commandHandlers; 72b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair private int clientDataPort = DEFAULT_CLIENT_DATA_PORT; 73b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair private InetAddress clientHost; 74b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair private InetAddress serverHost; 75b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair private Map attributes = new HashMap(); 76b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair private volatile boolean terminate = false; 77b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 78b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair /** 79b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * Create a new initialized instance 80b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * 81b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @param controlSocket - the control connection socket 82b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @param commandHandlers - the Map of command name -> CommandHandler. It is assumed that the 83b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * command names are all normalized to upper case. See {@link Command#normalizeName(String)}. 84b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair */ 85b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair public DefaultSession(Socket controlSocket, Map commandHandlers) { 86b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair Assert.notNull(controlSocket, "controlSocket"); 87b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair Assert.notNull(commandHandlers, "commandHandlers"); 88b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 89b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair this.controlSocket = controlSocket; 90b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair this.commandHandlers = commandHandlers; 91b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair this.serverHost = controlSocket.getLocalAddress(); 92b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 93b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 94b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair /** 95b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * Return the InetAddress representing the client host for this session 96b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * 97b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @return the client host 98b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @see org.mockftpserver.core.session.Session#getClientHost() 99b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair */ 100b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair public InetAddress getClientHost() { 101b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair return controlSocket.getInetAddress(); 102b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 103b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 104b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair /** 105b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * Return the InetAddress representing the server host for this session 106b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * 107b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @return the server host 108b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @see org.mockftpserver.core.session.Session#getServerHost() 109b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair */ 110b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair public InetAddress getServerHost() { 111b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair return serverHost; 112b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 113b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 114b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair /** 115b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * Send the specified reply code and text across the control connection. 116b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * The reply text is trimmed before being sent. 117b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * 118b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @param code - the reply code 119b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @param text - the reply text to send; may be null 120b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair */ 121b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair public void sendReply(int code, String text) { 122b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair assertValidReplyCode(code); 123b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 124b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair StringBuffer buffer = new StringBuffer(Integer.toString(code)); 125b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 126b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair if (text != null && text.length() > 0) { 127b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair String replyText = text.trim(); 128b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair if (replyText.indexOf("\n") != -1) { 129b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair int lastIndex = replyText.lastIndexOf("\n"); 130b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair buffer.append("-"); 131b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair for (int i = 0; i < replyText.length(); i++) { 132b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair char c = replyText.charAt(i); 133b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair buffer.append(c); 134b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair if (i == lastIndex) { 135b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair buffer.append(Integer.toString(code)); 136b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair buffer.append(" "); 137b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 138b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 139b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } else { 140b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair buffer.append(" "); 141b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair buffer.append(replyText); 142b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 143b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 144b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair LOG.debug("Sending Reply [" + buffer.toString() + "]"); 145b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair writeLineToControlConnection(buffer.toString()); 146b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 147b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 148b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair /** 149b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @see org.mockftpserver.core.session.Session#openDataConnection() 150b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair */ 151b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair public void openDataConnection() { 152b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair try { 153b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair if (passiveModeDataSocket != null) { 154b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair LOG.debug("Waiting for (passive mode) client connection from client host [" + clientHost 155b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair + "] on port " + passiveModeDataSocket.getLocalPort()); 156b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair // TODO set socket timeout 157b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair try { 158b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair dataSocket = passiveModeDataSocket.accept(); 159b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair LOG.debug("Successful (passive mode) client connection to port " 160b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair + passiveModeDataSocket.getLocalPort()); 161b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 162b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair catch (SocketTimeoutException e) { 163b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair throw new MockFtpServerException(e); 164b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 165b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } else { 166b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair Assert.notNull(clientHost, "clientHost"); 167b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair LOG.debug("Connecting to client host [" + clientHost + "] on data port [" + clientDataPort 168b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair + "]"); 169b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair dataSocket = socketFactory.createSocket(clientHost, clientDataPort); 170b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 171b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair dataOutputStream = dataSocket.getOutputStream(); 172b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair dataInputStream = dataSocket.getInputStream(); 173b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 174b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair catch (IOException e) { 175b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair throw new MockFtpServerException(e); 176b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 177b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 178b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 179b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair /** 180b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * Switch to passive mode 181b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * 182b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @return the local port to be connected to by clients for data transfers 183b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @see org.mockftpserver.core.session.Session#switchToPassiveMode() 184b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair */ 185b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair public int switchToPassiveMode() { 186b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair try { 187b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair passiveModeDataSocket = serverSocketFactory.createServerSocket(0); 188b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair return passiveModeDataSocket.getLocalPort(); 189b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 190b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair catch (IOException e) { 191b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair throw new MockFtpServerException("Error opening passive mode server data socket", e); 192b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 193b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 194b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 195b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair /** 196b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @see org.mockftpserver.core.session.Session#closeDataConnection() 197b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair */ 198b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair public void closeDataConnection() { 199b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair try { 200b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair LOG.debug("Flushing and closing client data socket"); 201b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair dataOutputStream.flush(); 202b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair dataOutputStream.close(); 203b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair dataInputStream.close(); 204b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair dataSocket.close(); 205b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 206b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair catch (IOException e) { 207b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair LOG.error("Error closing client data socket", e); 208b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 209b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 210b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 211b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair /** 212b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * Write a single line to the control connection, appending a newline 213b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * 214b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @param line - the line to write 215b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair */ 216b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair private void writeLineToControlConnection(String line) { 217b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair try { 218b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair controlConnectionWriter.write(line + END_OF_LINE); 219b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair controlConnectionWriter.flush(); 220b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 221b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair catch (IOException e) { 222b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair LOG.error("Error writing to control connection", e); 223b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair throw new MockFtpServerException("Error writing to control connection", e); 224b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 225b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 226b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 227b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair /** 228b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @see org.mockftpserver.core.session.Session#close() 229b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair */ 230b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair public void close() { 231b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair LOG.trace("close()"); 232b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair terminate = true; 233b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 234b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 235b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair /** 236b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @see org.mockftpserver.core.session.Session#sendData(byte[], int) 237b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair */ 238b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair public void sendData(byte[] data, int numBytes) { 239b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair Assert.notNull(data, "data"); 240b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair try { 241b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair dataOutputStream.write(data, 0, numBytes); 242b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 243b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair catch (IOException e) { 244b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair throw new MockFtpServerException(e); 245b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 246b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 247b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 248b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair /** 249b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @see org.mockftpserver.core.session.Session#readData() 250b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair */ 251b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair public byte[] readData() { 252b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 253b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair ByteArrayOutputStream bytes = new ByteArrayOutputStream(); 254b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 255b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair try { 256b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair while (true) { 257b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair int b = dataInputStream.read(); 258b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair if (b == -1) { 259b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair break; 260b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 261b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair bytes.write(b); 262b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 263b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair return bytes.toByteArray(); 264b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 265b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair catch (IOException e) { 266b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair throw new MockFtpServerException(e); 267b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 268b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 269b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 270b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair /** 271b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * Wait for and read the command sent from the client on the control connection. 272b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * 273b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @return the Command sent from the client; may be null if the session has been closed 274b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * <p/> 275b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * Package-private to enable testing 276b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair */ 277b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair Command readCommand() { 278b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 279b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair final long socketReadIntervalMilliseconds = 100L; 280b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 281b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair try { 282b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair while (true) { 283b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair if (terminate) { 284b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair return null; 285b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 286b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair // Don't block; only read command when it is available 287b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair if (controlConnectionReader.ready()) { 288b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair String command = controlConnectionReader.readLine(); 289b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair LOG.info("Received command: [" + command + "]"); 290b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair return parseCommand(command); 291b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 292b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair try { 293b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair Thread.sleep(socketReadIntervalMilliseconds); 294b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 295b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair catch (InterruptedException e) { 296b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair throw new MockFtpServerException(e); 297b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 298b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 299b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 300b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair catch (IOException e) { 301b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair LOG.error("Read failed", e); 302b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair throw new MockFtpServerException(e); 303b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 304b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 305b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 306b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair /** 307b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * Parse the command String into a Command object 308b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * 309b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @param commandString - the command String 310b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @return the Command object parsed from the command String 311b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair */ 312b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair Command parseCommand(String commandString) { 313b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair Assert.notNullOrEmpty(commandString, "commandString"); 314b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 315b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair List parameters = new ArrayList(); 316b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair String name; 317b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 318b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair int indexOfFirstSpace = commandString.indexOf(" "); 319b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair if (indexOfFirstSpace != -1) { 320b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair name = commandString.substring(0, indexOfFirstSpace); 321b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair StringTokenizer tokenizer = new StringTokenizer(commandString.substring(indexOfFirstSpace + 1), 322b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair ","); 323b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair while (tokenizer.hasMoreTokens()) { 324b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair parameters.add(tokenizer.nextToken()); 325b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 326b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } else { 327b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair name = commandString; 328b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 329b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 330b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair String[] parametersArray = new String[parameters.size()]; 331b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair return new Command(name, (String[]) parameters.toArray(parametersArray)); 332b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 333b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 334b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair /** 335b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @see org.mockftpserver.core.session.Session#setClientDataHost(java.net.InetAddress) 336b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair */ 337b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair public void setClientDataHost(InetAddress clientHost) { 338b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair this.clientHost = clientHost; 339b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 340b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 341b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair /** 342b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @see org.mockftpserver.core.session.Session#setClientDataPort(int) 343b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair */ 344b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair public void setClientDataPort(int dataPort) { 345b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair this.clientDataPort = dataPort; 346b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 347b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair // Clear out any passive data connection mode information 348b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair if (passiveModeDataSocket != null) { 349b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair try { 350b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair this.passiveModeDataSocket.close(); 351b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 352b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair catch (IOException e) { 353b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair throw new MockFtpServerException(e); 354b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 355b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair passiveModeDataSocket = null; 356b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 357b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 358b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 359b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair /** 360b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @see java.lang.Runnable#run() 361b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair */ 362b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair public void run() { 363b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair try { 364b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 365b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair InputStream inputStream = controlSocket.getInputStream(); 366b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair OutputStream outputStream = controlSocket.getOutputStream(); 367b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair controlConnectionReader = new BufferedReader(new InputStreamReader(inputStream)); 368b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair controlConnectionWriter = new PrintWriter(outputStream, true); 369b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 370b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair LOG.debug("Starting the session..."); 371b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 372b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair CommandHandler connectCommandHandler = (CommandHandler) commandHandlers.get(CommandNames.CONNECT); 373b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair connectCommandHandler.handleCommand(new Command(CommandNames.CONNECT, new String[0]), this); 374b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 375b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair while (!terminate) { 376b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair readAndProcessCommand(); 377b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 378b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 379b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair catch (Exception e) { 380b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair LOG.error(e); 381b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair throw new MockFtpServerException(e); 382b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 383b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair finally { 384b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair LOG.debug("Cleaning up the session"); 385b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair try { 386b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair controlConnectionReader.close(); 387b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair controlConnectionWriter.close(); 388b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 389b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair catch (IOException e) { 390b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair LOG.error(e); 391b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 392b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair LOG.debug("Session stopped."); 393b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 394b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 395b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 396b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair /** 397b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * Read and process the next command from the control connection 398b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * 399b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @throws Exception - if any error occurs 400b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair */ 401b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair private void readAndProcessCommand() throws Exception { 402b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 403b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair Command command = readCommand(); 404b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair if (command != null) { 405b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair String normalizedCommandName = Command.normalizeName(command.getName()); 406b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair CommandHandler commandHandler = (CommandHandler) commandHandlers.get(normalizedCommandName); 407b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 408b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair if (commandHandler == null) { 409b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair commandHandler = (CommandHandler) commandHandlers.get(CommandNames.UNSUPPORTED); 410b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 411b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 412b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair Assert.notNull(commandHandler, "CommandHandler for command [" + normalizedCommandName + "]"); 413b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair commandHandler.handleCommand(command, this); 414b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 415b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 416b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 417b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair /** 418b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * Assert that the specified number is a valid reply code 419b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * 420b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @param replyCode - the reply code to check 421b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair */ 422b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair private void assertValidReplyCode(int replyCode) { 423b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair Assert.isTrue(replyCode > 0, "The number [" + replyCode + "] is not a valid reply code"); 424b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 425b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 426b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair /** 427b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * Return the attribute value for the specified name. Return null if no attribute value 428b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * exists for that name or if the attribute value is null. 429b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * 430b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @param name - the attribute name; may not be null 431b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @return the value of the attribute stored under name; may be null 432b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @see org.mockftpserver.core.session.Session#getAttribute(java.lang.String) 433b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair */ 434b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair public Object getAttribute(String name) { 435b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair Assert.notNull(name, "name"); 436b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair return attributes.get(name); 437b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 438b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 439b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair /** 440b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * Store the value under the specified attribute name. 441b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * 442b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @param name - the attribute name; may not be null 443b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @param value - the attribute value; may be null 444b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @see org.mockftpserver.core.session.Session#setAttribute(java.lang.String, java.lang.Object) 445b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair */ 446b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair public void setAttribute(String name, Object value) { 447b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair Assert.notNull(name, "name"); 448b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair attributes.put(name, value); 449b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 450b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 451b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair /** 452b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * Return the Set of names under which attributes have been stored on this session. 453b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * Returns an empty Set if no attribute values are stored. 454b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * 455b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @return the Set of attribute names 456b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @see org.mockftpserver.core.session.Session#getAttributeNames() 457b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair */ 458b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair public Set getAttributeNames() { 459b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair return attributes.keySet(); 460b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 461b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 462b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair /** 463b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * Remove the attribute value for the specified name. Do nothing if no attribute 464b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * value is stored for the specified name. 465b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * 466b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @param name - the attribute name; may not be null 467b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @throws AssertFailedException - if name is null 468b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair * @see org.mockftpserver.core.session.Session#removeAttribute(java.lang.String) 469b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair */ 470b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair public void removeAttribute(String name) { 471b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair Assert.notNull(name, "name"); 472b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair attributes.remove(name); 473b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair } 474b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair 475b2f4a2dfc590c250e42b21eb40d9539ac135b495chrismair} 476