blob: 25bb23cca4a927862f0b80a310f2d9661da50d3f [file] [log] [blame]
chrismair00dc7bd2014-05-11 21:21:28 +00001/*
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}