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