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.fake.ServerConfigurationAware
19import org.mockftpserver.fake.ServerConfiguration
20import org.mockftpserver.fake.filesystem.FileSystem
21import org.mockftpserver.fake.filesystem.ExistingFileOperationException
22import org.mockftpserver.fake.filesystem.NewFileOperationException
23import org.mockftpserver.core.CommandSyntaxException
24import org.mockftpserver.core.IllegalStateException
25import org.mockftpserver.core.NotLoggedInException
26import org.mockftpserver.core.command.Command
27import org.mockftpserver.core.command.CommandHandler
28import org.mockftpserver.core.command.ReplyCodes
29import org.mockftpserver.core.session.Session
30import org.mockftpserver.core.session.SessionKeys
31import org.apache.log4j.Logger
32import java.text.MessageFormat
33import org.mockftpserver.fake.filesystem.InvalidFilenameException
34
35/**
36 * Abstract superclass for CommandHandler classes for the "Fake" server.
37 *
38 * @version $Revision: $ - $Date: $
39 *
40 * @author Chris Mair
41 */
42abstract class AbstractFakeCommandHandler implements CommandHandler, ServerConfigurationAware {
43
44     final Logger LOG = Logger.getLogger(this.class)
45     ServerConfiguration serverConfiguration
46
47     /**
48      * Use template method to centralize and ensure common validation
49      */
50     void handleCommand(Command command, Session session) {
51         assert serverConfiguration != null
52         assert command != null
53         assert session != null
54
55         try {
56             handle(command, session)
57         }
58         catch(CommandSyntaxException e) {
59             LOG.warn("Error handling command: $command; ${e}")
60             sendReply(session, ReplyCodes.COMMAND_SYNTAX_ERROR)
61         }
62         catch(IllegalStateException e) {
63             LOG.warn("Error handling command: $command; ${e}")
64             sendReply(session, ReplyCodes.ILLEGAL_STATE)
65         }
66         catch(NotLoggedInException e) {
67             LOG.warn("Error handling command: $command; ${e}")
68             sendReply(session, ReplyCodes.NOT_LOGGED_IN)
69         }
70         catch(ExistingFileOperationException e) {
71             LOG.warn("Error handling command: $command; ${e}; path: ${e.path}")
72             sendReply(session, ReplyCodes.EXISTING_FILE_ERROR, [e.path])
73         }
74         catch(NewFileOperationException e) {
75             LOG.warn("Error handling command: $command; ${e}; path: ${e.path}")
76             sendReply(session, ReplyCodes.NEW_FILE_ERROR, [e.path])
77         }
78         catch(InvalidFilenameException e) {
79             e.printStackTrace()
80             LOG.warn("Error handling command: $command; ${e}")
81             sendReply(session, ReplyCodes.FILENAME_NOT_VALID, [e.path])
82         }
83     }
84
85     /**
86      * Convenience method to return the FileSystem stored in the ServerConfiguration
87      */
88     protected FileSystem getFileSystem() {
89         serverConfiguration.fileSystem
90     }
91
92     /**
93      * Subclasses must implement this
94      */
95     protected abstract void handle(Command command, Session session)
96
97     // -------------------------------------------------------------------------
98     // Utility methods for subclasses
99     // -------------------------------------------------------------------------
100
101     /**
102      * Send a reply for this command on the control connection.
103      *
104      * The reply code is designated by the <code>replyCode</code> property, and the reply text
105      * is retrieved from the <code>replyText</code> ResourceBundle, using the reply code as the key.
106      *
107      * @param session - the Session
108      * @param replyCode - the reply code
109      * @param args - the optional message arguments; defaults to []
110      *
111      * @throws AssertionError - if session is null
112      *
113      * @see MessageFormat
114      */
115     protected void sendReply(Session session, int replyCode, args = []) {
116         assert session
117         assertValidReplyCode(replyCode);
118
119         String key = Integer.toString(replyCode);
120         String text = serverConfiguration.getTextForReplyCode(replyCode)
121
122         String replyText = (args) ? MessageFormat.format(text, args as Object[]) : text;
123
124         String replyTextToLog = (replyText == null) ? "" : " " + replyText;
125         // TODO change to LOG.debug()
126         def argsToLog = (args) ? " args=$args" : ""
127         LOG.info("Sending reply [" + replyCode + replyTextToLog + "]" + argsToLog);
128         session.sendReply(replyCode, replyText);
129     }
130
131     /**
132      * Assert that the specified number is a valid reply code
133      * @param replyCode - the reply code to check
134      *
135      * @throws AssertionError - if the replyCode is invalid
136      */
137     protected void assertValidReplyCode(int replyCode) {
138         assert replyCode > 0, "The number [" + replyCode + "] is not a valid reply code"
139     }
140
141     /**
142      * Return the value of the command's parameter at the specified index.
143      * @param command - the Command
144      * @param index - the index of the parameter to retrieve; defaults to zero
145      * @return the value of the command parameter
146      * @throws CommandSyntaxException if the Command does not have a parameter at that index
147      */
148     protected String getRequiredParameter(Command command, int index=0) {
149         String value = command.getParameter(index)
150         if (!value) {
151             throw new CommandSyntaxException("$command missing required parameter at index [$index]")
152         }
153         return value
154     }
155
156     /**
157      * Return the value of the named attribute within the session.
158      * @param session - the Session
159      * @param name - the name of the session attribute to retrieve
160      * @return the value of the named session attribute
161      * @throws IllegalStateException - if the Session does not contain the named attribute
162      */
163     protected Object getRequiredSessionAttribute(Session session, String name) {
164         Object value = session.getAttribute(name)
165         if (value == null) {
166             throw new IllegalStateException("Session missing required attribute [$name]")
167         }
168         return value
169     }
170
171     /**
172      * Verify that the current user (if any) has already logged in successfully.
173      * @param session - the Session
174      */
175     protected void verifyLoggedIn(Session session) {
176         if (session.getAttribute(SessionKeys.USER_ACCOUNT) == null) {
177             throw new NotLoggedInException("User has not logged in")
178         }
179     }
180
181     /**
182      * Verify that the specified condition related to an existing file is true,
183      * otherwise throw a ExistingFileOperationException.
184      *
185      * @param condition - the condition that must be true
186      * @param path - the path involved in the operation; this will be included in the
187      * 		error message if the condition is not true.
188      * @throws ExistingFileOperationException - if the condition is not true
189      */
190     protected void verifyForExistingFile(condition, path) {
191         if (!condition) {
192             throw new ExistingFileOperationException(path)
193         }
194     }
195
196     /**
197      * Verify that the specified condition related to a new file is true,
198      * otherwise throw a NewFileOperationException.
199      *
200      * @param condition - the condition that must be true
201      * @param path - the path involved in the operation; this will be included in the
202      * 		error message if the condition is not true.
203      * @throws NewFileOperationException - if the condition is not true
204      */
205     protected void verifyForNewFile(condition, path) {
206         if (!condition) {
207             throw new NewFileOperationException(path)
208         }
209     }
210
211     /**
212      * Return the full, absolute path for the specified abstract pathname.
213      * If path is null, return the current directory (stored in the session). If
214      * path represents an absolute path, then return path as is. Otherwise, path
215      * is relative, so assemble the full path from the current directory
216      * and the specified relative path.
217      * @param Session - the Session
218      * @param path - the abstract pathname; may be null
219      * @return the resulting full, absolute path
220      */
221     protected String getRealPath(Session session, String path) {
222         def currentDirectory = session.getAttribute(SessionKeys.CURRENT_DIRECTORY)
223         if (path == null) {
224             return currentDirectory
225         }
226         if (fileSystem.isAbsolute(path)) {
227             return path
228         }
229         return fileSystem.path(currentDirectory, path)
230     }
231
232     /**
233      * Return the end-of-line character(s) used when building multi-line responses
234      */
235     protected String endOfLine() {
236         "\n"
237     }
238}