AbstractFakeCommandHandler.java revision e05d3ea57d7e78312ed9a25b6c50a4b1258fd5d2
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(command, session, e, ReplyCodes.FILENAME_NOT_VALID, list(e.getPath()));
91        }
92        catch (FileSystemException e) {
93            handleFileSystemException(command, 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 command   - the Command
206     * @param session   - the Session
207     * @param exception - the caught exception
208     * @param replyCode - the reply code that should be sent back
209     * @param arg       - the arg for the reply (message)
210     */
211    private void handleFileSystemException(Command command, Session session, FileSystemException exception, int replyCode, Object arg) {
212        LOG.warn("Error handling command: " + command + "; " + exception, exception);
213        sendReply(session, replyCode, exception.getMessageKey(), Collections.singletonList(arg));
214    }
215
216    /**
217     * Assert that the specified number is a valid reply code
218     *
219     * @param replyCode - the reply code to check
220     * @throws AssertionError - if the replyCode is invalid
221     */
222    protected void assertValidReplyCode(int replyCode) {
223        Assert.isTrue(replyCode > 0, "The replyCode [" + replyCode + "] is not valid");
224    }
225
226    /**
227     * Return the value of the named attribute within the session.
228     *
229     * @param session - the Session
230     * @param name    - the name of the session attribute to retrieve
231     * @return the value of the named session attribute
232     * @throws IllegalStateException - if the Session does not contain the named attribute
233     */
234    protected Object getRequiredSessionAttribute(Session session, String name) {
235        Object value = session.getAttribute(name);
236        if (value == null) {
237            throw new IllegalStateException("Session missing required attribute [" + name + "]");
238        }
239        return value;
240    }
241
242    /**
243     * Verify that the current user (if any) has already logged in successfully.
244     *
245     * @param session - the Session
246     */
247    protected void verifyLoggedIn(Session session) {
248        if (getUserAccount(session) == null) {
249            throw new NotLoggedInException("User has not logged in");
250        }
251    }
252
253    /**
254     * @param session - the Session
255     * @return the UserAccount stored in the specified session; may be null
256     */
257    protected UserAccount getUserAccount(Session session) {
258        return (UserAccount) session.getAttribute(SessionKeys.USER_ACCOUNT);
259    }
260
261    /**
262     * Verify that the specified condition related to the file system is true,
263     * otherwise throw a FileSystemException.
264     *
265     * @param condition  - the condition that must be true
266     * @param path       - the path involved in the operation; this will be included in the
267     *                   error message if the condition is not true.
268     * @param messageKey - the message key for the exception message
269     * @throws FileSystemException - if the condition is not true
270     */
271    protected void verifyFileSystemCondition(boolean condition, String path, String messageKey) {
272        if (!condition) {
273            throw new FileSystemException(path, messageKey);
274        }
275    }
276
277    /**
278     * Verify that the current user has execute permission to the specified path
279     *
280     * @param session - the Session
281     * @param path    - the file system path
282     * @throws FileSystemException - if the condition is not true
283     */
284    protected void verifyExecutePermission(Session session, String path) {
285        UserAccount userAccount = getUserAccount(session);
286        FileSystemEntry entry = getFileSystem().getEntry(path);
287        verifyFileSystemCondition(userAccount.canExecute(entry), path, "filesystem.cannotExecute");
288    }
289
290    /**
291     * Verify that the current user has write permission to the specified path
292     *
293     * @param session - the Session
294     * @param path    - the file system path
295     * @throws FileSystemException - if the condition is not true
296     */
297    protected void verifyWritePermission(Session session, String path) {
298        UserAccount userAccount = getUserAccount(session);
299        FileSystemEntry entry = getFileSystem().getEntry(path);
300        verifyFileSystemCondition(userAccount.canWrite(entry), path, "filesystem.cannotWrite");
301    }
302
303    /**
304     * Verify that the current user has read permission to the specified path
305     *
306     * @param session - the Session
307     * @param path    - the file system path
308     * @throws FileSystemException - if the condition is not true
309     */
310    protected void verifyReadPermission(Session session, String path) {
311        UserAccount userAccount = getUserAccount(session);
312        FileSystemEntry entry = getFileSystem().getEntry(path);
313        verifyFileSystemCondition(userAccount.canRead(entry), path, "filesystem.cannotRead");
314    }
315
316    /**
317     * Return the full, absolute path for the specified abstract pathname.
318     * If path is null, return the current directory (stored in the session). If
319     * path represents an absolute path, then return path as is. Otherwise, path
320     * is relative, so assemble the full path from the current directory
321     * and the specified relative path.
322     *
323     * @param session - the Session
324     * @param path    - the abstract pathname; may be null
325     * @return the resulting full, absolute path
326     */
327    protected String getRealPath(Session session, String path) {
328        String currentDirectory = (String) session.getAttribute(SessionKeys.CURRENT_DIRECTORY);
329        if (path == null) {
330            return currentDirectory;
331        }
332        if (getFileSystem().isAbsolute(path)) {
333            return path;
334        }
335        return getFileSystem().path(currentDirectory, path);
336    }
337
338    /**
339     * Return the end-of-line character(s) used when building multi-line responses
340     */
341    protected String endOfLine() {
342        return "\r\n";
343    }
344
345    private String getTextForKey(String key) {
346        String msgKey = (key != null) ? key : INTERNAL_ERROR_KEY;
347        try {
348            return serverConfiguration.getReplyTextBundle().getString(msgKey);
349        }
350        catch (MissingResourceException e) {
351            // No reply text is mapped for the specified key
352            LOG.warn("No reply text defined for key [" + msgKey + "]");
353            return null;
354        }
355    }
356
357    // -------------------------------------------------------------------------
358    // Login Support (used by USER and PASS commands)
359    // -------------------------------------------------------------------------
360
361    /**
362     * Validate the UserAccount for the specified username. If valid, return true. If the UserAccount does
363     * not exist or is invalid, log an error message, send back a reply code of 530 with an appropriate
364     * error message, and return false. A UserAccount is considered invalid if the homeDirectory property
365     * is not set or is set to a non-existent directory.
366     *
367     * @param username - the username
368     * @param session  - the session; used to send back an error reply if necessary
369     * @return true only if the UserAccount for the named user is valid
370     */
371    protected boolean validateUserAccount(String username, Session session) {
372        UserAccount userAccount = serverConfiguration.getUserAccount(username);
373        if (userAccount == null || !userAccount.isValid()) {
374            LOG.error("UserAccount missing or not valid for username [" + username + "]: " + userAccount);
375            sendReply(session, ReplyCodes.USER_ACCOUNT_NOT_VALID, "login.userAccountNotValid", list(username));
376            return false;
377        }
378
379        String home = userAccount.getHomeDirectory();
380        if (!getFileSystem().isDirectory(home)) {
381            LOG.error("Home directory configured for username [" + username + "] is not valid: " + home);
382            sendReply(session, ReplyCodes.USER_ACCOUNT_NOT_VALID, "login.homeDirectoryNotValid", list(username, home));
383            return false;
384        }
385
386        return true;
387    }
388
389    /**
390     * Log in the specified user for the current session. Send back a reply of 230 with a message indicated
391     * by the replyMessageKey and set the UserAccount and current directory (homeDirectory) in the session.
392     *
393     * @param userAccount     - the userAccount for the user to be logged in
394     * @param session         - the session
395     * @param replyCode       - the reply code to send
396     * @param replyMessageKey - the message key for the reply text
397     */
398    protected void login(UserAccount userAccount, Session session, int replyCode, String replyMessageKey) {
399        sendReply(session, replyCode, replyMessageKey);
400        session.setAttribute(SessionKeys.USER_ACCOUNT, userAccount);
401        session.setAttribute(SessionKeys.CURRENT_DIRECTORY, userAccount.getHomeDirectory());
402    }
403
404    protected List list(Object item) {
405        return Collections.singletonList(item);
406    }
407
408    protected List list(Object item1, Object item2) {
409        List list = new ArrayList(2);
410        list.add(item1);
411        list.add(item2);
412        return list;
413    }
414
415    protected boolean notNullOrEmpty(String string) {
416        return string != null && string.length() > 0;
417    }
418
419    protected String defaultIfNullOrEmpty(String string, String defaultString) {
420        return (notNullOrEmpty(string) ? string : defaultString);
421    }
422
423}