AbstractFakeCommandHandler.java revision 1845e1232d9bfd95dcb223ca3ffa7a5b49dda85e
1/* 2 * Copyright 2008 the original author or authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16package org.mockftpserver.fake.command; 17 18import org.apache.log4j.Logger; 19import org.mockftpserver.core.CommandSyntaxException; 20import org.mockftpserver.core.IllegalStateException; 21import org.mockftpserver.core.NotLoggedInException; 22import org.mockftpserver.core.command.Command; 23import org.mockftpserver.core.command.CommandHandler; 24import org.mockftpserver.core.command.ReplyCodes; 25import org.mockftpserver.core.session.Session; 26import org.mockftpserver.core.session.SessionKeys; 27import org.mockftpserver.core.util.Assert; 28import org.mockftpserver.fake.filesystem.FileSystem; 29import org.mockftpserver.fake.filesystem.FileSystemEntry; 30import org.mockftpserver.fake.filesystem.FileSystemException; 31import org.mockftpserver.fake.filesystem.InvalidFilenameException; 32import org.mockftpserver.fake.server.ServerConfiguration; 33import org.mockftpserver.fake.server.ServerConfigurationAware; 34import org.mockftpserver.fake.user.UserAccount; 35 36import java.text.MessageFormat; 37import java.util.ArrayList; 38import java.util.Collections; 39import java.util.List; 40import java.util.MissingResourceException; 41 42/** 43 * Abstract superclass for CommandHandler classes for the "Fake" server. 44 * 45 * @author Chris Mair 46 * @version $Revision: 136 $ - $Date: 2008-10-23 22:17:29 -0400 (Thu, 23 Oct 2008) $ 47 */ 48public abstract class AbstractFakeCommandHandler implements CommandHandler, ServerConfigurationAware { 49 50 protected static final String INTERNAL_ERROR_KEY = "internalError"; 51 protected final Logger LOG = Logger.getLogger(this.getClass()); 52 53 private ServerConfiguration serverConfiguration; 54 55 /** 56 * Reply code sent back when a FileSystemException is caught by the {@link #handleCommand(Command, Session)} 57 * This defaults to ReplyCodes.EXISTING_FILE_ERROR (550). 58 */ 59 protected int replyCodeForFileSystemException = ReplyCodes.READ_FILE_ERROR; 60 61 public ServerConfiguration getServerConfiguration() { 62 return serverConfiguration; 63 } 64 65 public void setServerConfiguration(ServerConfiguration serverConfiguration) { 66 this.serverConfiguration = serverConfiguration; 67 } 68 69 /** 70 * Use template method to centralize and ensure common validation 71 */ 72 public void handleCommand(Command command, Session session) { 73 Assert.notNull(serverConfiguration, "serverConfiguration"); 74 Assert.notNull(command, "command"); 75 Assert.notNull(session, "session"); 76 77 try { 78 handle(command, session); 79 } 80 catch (CommandSyntaxException e) { 81 handleException(command, session, e, ReplyCodes.COMMAND_SYNTAX_ERROR); 82 } 83 catch (IllegalStateException e) { 84 handleException(command, session, e, ReplyCodes.ILLEGAL_STATE); 85 } 86 catch (NotLoggedInException e) { 87 handleException(command, session, e, ReplyCodes.NOT_LOGGED_IN); 88 } 89 catch (InvalidFilenameException e) { 90 handleFileSystemException(session, e, ReplyCodes.FILENAME_NOT_VALID, list(e.getPath())); 91 } 92 catch (FileSystemException e) { 93 handleFileSystemException(session, e, replyCodeForFileSystemException, list(e.getPath())); 94 } 95 } 96 97 /** 98 * Convenience method to return the FileSystem stored in the ServerConfiguration 99 * 100 * @return the FileSystem 101 */ 102 protected FileSystem getFileSystem() { 103 return serverConfiguration.getFileSystem(); 104 } 105 106 /** 107 * Subclasses must implement this 108 */ 109 protected abstract void handle(Command command, Session session); 110 111 // ------------------------------------------------------------------------- 112 // Utility methods for subclasses 113 // ------------------------------------------------------------------------- 114 115 /** 116 * Send a reply for this command on the control connection. 117 * <p/> 118 * The reply code is designated by the <code>replyCode</code> property, and the reply text 119 * is retrieved from the <code>replyText</code> ResourceBundle, using the specified messageKey. 120 * 121 * @param session - the Session 122 * @param replyCode - the reply code 123 * @param messageKey - the resource bundle key for the reply text 124 * @throws AssertionError - if session is null 125 * @see MessageFormat 126 */ 127 protected void sendReply(Session session, int replyCode, String messageKey) { 128 sendReply(session, replyCode, messageKey, Collections.EMPTY_LIST); 129 } 130 131 /** 132 * Send a reply for this command on the control connection. 133 * <p/> 134 * The reply code is designated by the <code>replyCode</code> property, and the reply text 135 * is retrieved from the <code>replyText</code> ResourceBundle, using the specified messageKey. 136 * 137 * @param session - the Session 138 * @param replyCode - the reply code 139 * @param messageKey - the resource bundle key for the reply text 140 * @param args - the optional message arguments; defaults to [] 141 * @throws AssertionError - if session is null 142 * @see MessageFormat 143 */ 144 protected void sendReply(Session session, int replyCode, String messageKey, List args) { 145 Assert.notNull(session, "session"); 146 assertValidReplyCode(replyCode); 147 148 String text = getTextForKey(messageKey); 149 String replyText = (args != null && !args.isEmpty()) ? MessageFormat.format(text, args.toArray()) : text; 150 151 String replyTextToLog = (replyText == null) ? "" : " " + replyText; 152 // TODO change to LOG.debug() 153 String argsToLog = (args != null && !args.isEmpty()) ? (" args=" + args) : ""; 154 LOG.info("Sending reply [" + replyCode + replyTextToLog + "]" + argsToLog); 155 session.sendReply(replyCode, replyText); 156 } 157 158 /** 159 * Send a reply for this command on the control connection. 160 * <p/> 161 * The reply code is designated by the <code>replyCode</code> property, and the reply text 162 * is retrieved from the <code>replyText</code> ResourceBundle, using the reply code as the key. 163 * 164 * @param session - the Session 165 * @param replyCode - the reply code 166 * @throws AssertionError - if session is null 167 * @see MessageFormat 168 */ 169 protected void sendReply(Session session, int replyCode) { 170 sendReply(session, replyCode, Collections.EMPTY_LIST); 171 } 172 173 /** 174 * Send a reply for this command on the control connection. 175 * <p/> 176 * The reply code is designated by the <code>replyCode</code> property, and the reply text 177 * is retrieved from the <code>replyText</code> ResourceBundle, using the reply code as the key. 178 * 179 * @param session - the Session 180 * @param replyCode - the reply code 181 * @param args - the optional message arguments; defaults to [] 182 * @throws AssertionError - if session is null 183 * @see MessageFormat 184 */ 185 protected void sendReply(Session session, int replyCode, List args) { 186 sendReply(session, replyCode, Integer.toString(replyCode), args); 187 } 188 189 /** 190 * Handle the exception caught during handleCommand() 191 * 192 * @param command - the Command 193 * @param session - the Session 194 * @param exception - the caught exception 195 * @param replyCode - the reply code that should be sent back 196 */ 197 private void handleException(Command command, Session session, Throwable exception, int replyCode) { 198 LOG.warn("Error handling command: " + command + "; " + exception, exception); 199 sendReply(session, replyCode); 200 } 201 202 /** 203 * Handle the exception caught during handleCommand() 204 * 205 * @param session - the Session 206 * @param exception - the caught exception 207 * @param replyCode - the reply code that should be sent back 208 * @param arg - the arg for the reply (message) 209 */ 210 private void handleFileSystemException(Session session, FileSystemException exception, int replyCode, Object arg) { 211 LOG.warn("Error handling command: $command; ${exception}", exception); 212 sendReply(session, replyCode, exception.getMessageKey(), Collections.singletonList(arg)); 213 } 214 215 /** 216 * Assert that the specified number is a valid reply code 217 * 218 * @param replyCode - the reply code to check 219 * @throws AssertionError - if the replyCode is invalid 220 */ 221 protected void assertValidReplyCode(int replyCode) { 222 Assert.isTrue(replyCode > 0, "The replyCode [" + replyCode + "] is not valid"); 223 } 224 225 /** 226 * Return the value of the named attribute within the session. 227 * 228 * @param session - the Session 229 * @param name - the name of the session attribute to retrieve 230 * @return the value of the named session attribute 231 * @throws IllegalStateException - if the Session does not contain the named attribute 232 */ 233 protected Object getRequiredSessionAttribute(Session session, String name) { 234 Object value = session.getAttribute(name); 235 if (value == null) { 236 throw new IllegalStateException("Session missing required attribute [" + name + "]"); 237 } 238 return value; 239 } 240 241 /** 242 * Verify that the current user (if any) has already logged in successfully. 243 * 244 * @param session - the Session 245 */ 246 protected void verifyLoggedIn(Session session) { 247 if (getUserAccount(session) == null) { 248 throw new NotLoggedInException("User has not logged in"); 249 } 250 } 251 252 /** 253 * @param session - the Session 254 * @return the UserAccount stored in the specified session; may be null 255 */ 256 protected UserAccount getUserAccount(Session session) { 257 return (UserAccount) session.getAttribute(SessionKeys.USER_ACCOUNT); 258 } 259 260 /** 261 * Verify that the specified condition related to the file system is true, 262 * otherwise throw a FileSystemException. 263 * 264 * @param condition - the condition that must be true 265 * @param path - the path involved in the operation; this will be included in the 266 * error message if the condition is not true. 267 * @param messageKey - the message key for the exception message 268 * @throws FileSystemException - if the condition is not true 269 */ 270 protected void verifyFileSystemCondition(boolean condition, String path, String messageKey) { 271 if (!condition) { 272 throw new FileSystemException(path, messageKey); 273 } 274 } 275 276 /** 277 * Verify that the current user has execute permission to the specified path 278 * 279 * @param session - the Session 280 * @param path - the file system path 281 * @throws FileSystemException - if the condition is not true 282 */ 283 protected void verifyExecutePermission(Session session, String path) { 284 UserAccount userAccount = getUserAccount(session); 285 FileSystemEntry entry = getFileSystem().getEntry(path); 286 verifyFileSystemCondition(userAccount.canExecute(entry), path, "filesystem.cannotExecute"); 287 } 288 289 /** 290 * Verify that the current user has write permission to the specified path 291 * 292 * @param session - the Session 293 * @param path - the file system path 294 * @throws FileSystemException - if the condition is not true 295 */ 296 protected void verifyWritePermission(Session session, String path) { 297 UserAccount userAccount = getUserAccount(session); 298 FileSystemEntry entry = getFileSystem().getEntry(path); 299 verifyFileSystemCondition(userAccount.canWrite(entry), path, "filesystem.cannotWrite"); 300 } 301 302 /** 303 * Verify that the current user has read permission to the specified path 304 * 305 * @param session - the Session 306 * @param path - the file system path 307 * @throws FileSystemException - if the condition is not true 308 */ 309 protected void verifyReadPermission(Session session, String path) { 310 UserAccount userAccount = getUserAccount(session); 311 FileSystemEntry entry = getFileSystem().getEntry(path); 312 verifyFileSystemCondition(userAccount.canRead(entry), path, "filesystem.cannotRead"); 313 } 314 315 /** 316 * Return the full, absolute path for the specified abstract pathname. 317 * If path is null, return the current directory (stored in the session). If 318 * path represents an absolute path, then return path as is. Otherwise, path 319 * is relative, so assemble the full path from the current directory 320 * and the specified relative path. 321 * 322 * @param session - the Session 323 * @param path - the abstract pathname; may be null 324 * @return the resulting full, absolute path 325 */ 326 protected String getRealPath(Session session, String path) { 327 String currentDirectory = (String) session.getAttribute(SessionKeys.CURRENT_DIRECTORY); 328 if (path == null) { 329 return currentDirectory; 330 } 331 if (getFileSystem().isAbsolute(path)) { 332 return path; 333 } 334 return getFileSystem().path(currentDirectory, path); 335 } 336 337 /** 338 * Return the end-of-line character(s) used when building multi-line responses 339 */ 340 protected String endOfLine() { 341 return "\r\n"; 342 } 343 344 private String getTextForKey(String key) { 345 String msgKey = (key != null) ? key : INTERNAL_ERROR_KEY; 346 try { 347 return serverConfiguration.getReplyTextBundle().getString(msgKey); 348 } 349 catch (MissingResourceException e) { 350 // No reply text is mapped for the specified key 351 LOG.warn("No reply text defined for key [" + msgKey + "]"); 352 return null; 353 } 354 } 355 356 // ------------------------------------------------------------------------- 357 // Login Support (used by USER and PASS commands) 358 // ------------------------------------------------------------------------- 359 360 /** 361 * Validate the UserAccount for the specified username. If valid, return true. If the UserAccount does 362 * not exist or is invalid, log an error message, send back a reply code of 530 with an appropriate 363 * error message, and return false. A UserAccount is considered invalid if the homeDirectory property 364 * is not set or is set to a non-existent directory. 365 * 366 * @param username - the username 367 * @param session - the session; used to send back an error reply if necessary 368 * @return true only if the UserAccount for the named user is valid 369 */ 370 protected boolean validateUserAccount(String username, Session session) { 371 UserAccount userAccount = serverConfiguration.getUserAccount(username); 372 if (userAccount == null || !userAccount.isValid()) { 373 LOG.error("UserAccount missing or not valid for username [" + username + "]: " + userAccount); 374 sendReply(session, ReplyCodes.USER_ACCOUNT_NOT_VALID, "login.userAccountNotValid", list(username)); 375 return false; 376 } 377 378 String home = userAccount.getHomeDirectory(); 379 if (!getFileSystem().isDirectory(home)) { 380 LOG.error("Home directory configured for username [" + username + "] is not valid: " + home); 381 sendReply(session, ReplyCodes.USER_ACCOUNT_NOT_VALID, "login.homeDirectoryNotValid", list(username, home)); 382 return false; 383 } 384 385 return true; 386 } 387 388 /** 389 * Log in the specified user for the current session. Send back a reply of 230 with a message indicated 390 * by the replyMessageKey and set the UserAccount and current directory (homeDirectory) in the session. 391 * 392 * @param userAccount - the userAccount for the user to be logged in 393 * @param session - the session 394 * @param replyCode - the reply code to send 395 * @param replyMessageKey - the message key for the reply text 396 */ 397 protected void login(UserAccount userAccount, Session session, int replyCode, String replyMessageKey) { 398 sendReply(session, replyCode, replyMessageKey); 399 session.setAttribute(SessionKeys.USER_ACCOUNT, userAccount); 400 session.setAttribute(SessionKeys.CURRENT_DIRECTORY, userAccount.getHomeDirectory()); 401 } 402 403 protected List list(Object item) { 404 return Collections.singletonList(item); 405 } 406 407 protected List list(Object item1, Object item2) { 408 List list = new ArrayList(2); 409 list.add(item1); 410 list.add(item2); 411 return list; 412 } 413 414 protected boolean notNullOrEmpty(String string) { 415 return string != null && string.length() > 0; 416 } 417 418 protected String defaultIfNullOrEmpty(String string, String defaultString) { 419 return (notNullOrEmpty(string) ? string : defaultString); 420 } 421 422}