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.mockftpserver.core.CommandSyntaxException;
19import org.mockftpserver.core.IllegalStateException;
20import org.mockftpserver.core.NotLoggedInException;
21import org.mockftpserver.core.command.AbstractCommandHandler;
22import org.mockftpserver.core.command.Command;
23import org.mockftpserver.core.command.ReplyCodes;
24import org.mockftpserver.core.session.Session;
25import org.mockftpserver.core.session.SessionKeys;
26import org.mockftpserver.core.util.Assert;
27import org.mockftpserver.fake.ServerConfiguration;
28import org.mockftpserver.fake.ServerConfigurationAware;
29import org.mockftpserver.fake.UserAccount;
30import org.mockftpserver.fake.filesystem.FileSystem;
31import org.mockftpserver.fake.filesystem.FileSystemEntry;
32import org.mockftpserver.fake.filesystem.FileSystemException;
33import org.mockftpserver.fake.filesystem.InvalidFilenameException;
34
35import java.text.MessageFormat;
36import java.util.ArrayList;
37import java.util.Collections;
38import java.util.List;
39import 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 */
47public 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}