AbstractCommandHandler.java revision 79eb164945510f6fbb16bdd3b0c1fcdf0b7f72ef
1/*
2 * Copyright 2007 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.core.command;
17
18import org.apache.log4j.Logger;
19import org.mockftpserver.core.CommandSyntaxException;
20import org.mockftpserver.core.session.Session;
21import org.mockftpserver.core.util.Assert;
22import org.mockftpserver.core.util.AssertFailedException;
23
24import java.text.MessageFormat;
25import java.util.ArrayList;
26import java.util.List;
27import java.util.MissingResourceException;
28import java.util.ResourceBundle;
29
30/**
31 * The abstract superclass for CommandHandler classes. This class manages the List of
32 * InvocationRecord objects corresponding to each invocation of the command handler,
33 * and provides helper methods for subclasses.
34 *
35 * @author Chris Mair
36 * @version $Revision$ - $Date$
37 */
38public abstract class AbstractCommandHandler implements CommandHandler, ReplyTextBundleAware, InvocationHistory {
39
40    private static final Logger LOG = Logger.getLogger(AbstractCommandHandler.class);
41
42    private ResourceBundle replyTextBundle;
43    private List invocations = new ArrayList();
44
45    // -------------------------------------------------------------------------
46    // Template Method
47    // -------------------------------------------------------------------------
48
49    /**
50     * Handle the specified command for the session. This method is declared to throw Exception,
51     * allowing CommandHandler implementations to avoid unnecessary exception-handling. All checked
52     * exceptions are expected to be wrapped and handled by the caller.
53     *
54     * @param command - the Command to be handled
55     * @param session - the session on which the Command was submitted
56     * @throws Exception
57     * @throws AssertFailedException - if the command or session is null
58     * @see org.mockftpserver.core.command.CommandHandler#handleCommand(org.mockftpserver.core.command.Command,
59     *      org.mockftpserver.core.session.Session)
60     */
61    public final void handleCommand(Command command, Session session) throws Exception {
62        Assert.notNull(command, "command");
63        Assert.notNull(session, "session");
64        InvocationRecord invocationRecord = new InvocationRecord(command, session.getClientHost());
65        invocations.add(invocationRecord);
66        try {
67            handleCommand(command, session, invocationRecord);
68        }
69        catch (CommandSyntaxException e) {
70            sendReply(session, ReplyCodes.COMMAND_SYNTAX_ERROR, null, null, null);
71        }
72        invocationRecord.lock();
73    }
74
75    /**
76     * Handle the specified command for the session. This method is declared to throw Exception,
77     * allowing CommandHandler implementations to avoid unnecessary exception-handling. All checked
78     * exceptions are expected to be wrapped and handled by the caller.
79     *
80     * @param command          - the Command to be handled
81     * @param session          - the session on which the Command was submitted
82     * @param invocationRecord - the InvocationRecord; CommandHandlers are expected to add
83     *                         handler-specific data to the InvocationRecord, as appropriate
84     * @throws Exception
85     */
86    protected abstract void handleCommand(Command command, Session session, InvocationRecord invocationRecord)
87            throws Exception;
88
89    //-------------------------------------------------------------------------
90    // Support for reply text ResourceBundle
91    //-------------------------------------------------------------------------
92
93    /**
94     * Return the ResourceBundle containing the reply text messages
95     *
96     * @return the replyTextBundle
97     * @see org.mockftpserver.core.command.ReplyTextBundleAware#getReplyTextBundle()
98     */
99    public ResourceBundle getReplyTextBundle() {
100        return replyTextBundle;
101    }
102
103    /**
104     * Set the ResourceBundle containing the reply text messages
105     *
106     * @param replyTextBundle - the replyTextBundle to set
107     * @see org.mockftpserver.core.command.ReplyTextBundleAware#setReplyTextBundle(java.util.ResourceBundle)
108     */
109    public void setReplyTextBundle(ResourceBundle replyTextBundle) {
110        this.replyTextBundle = replyTextBundle;
111    }
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 determined by the following rules:
122     * <ol>
123     * <li>If the <code>replyText</code> property is non-null, then use that.</li>
124     * <li>Otherwise, if <code>replyMessageKey</code> is non-null, the use that to retrieve a
125     * localized message from the <code>replyText</code> ResourceBundle.</li>
126     * <li>Otherwise, retrieve the reply text from the <code>replyText</code> ResourceBundle,
127     * using the reply code as the key.</li>
128     * </ol>
129     * If the arguments Object[] is not null, then these arguments are substituted within the
130     * reply text using the {@link MessageFormat} class.
131     *
132     * @param session         - the Session
133     * @param replyCode       - the reply code
134     * @param replyMessageKey - if not null (and replyText is null), this is used as the ResourceBundle
135     *                        message key instead of the reply code.
136     * @param replyText       - if non-null, this is used as the reply text
137     * @param arguments       - the array of arguments to be formatted and substituted within the reply
138     *                        text; may be null
139     * @throws AssertFailedException - if session is null
140     * @see MessageFormat
141     */
142    protected void sendReply(Session session, int replyCode, String replyMessageKey, String replyText,
143                             Object[] arguments) {
144
145        Assert.notNull(session, "session");
146        assertValidReplyCode(replyCode);
147
148        final Logger HANDLER_LOG = Logger.getLogger(getClass());
149        String key = (replyMessageKey != null) ? replyMessageKey : Integer.toString(replyCode);
150        String text = getTextForReplyCode(replyCode, key, replyText, arguments);
151        String replyTextToLog = (text == null) ? "" : " " + text;
152        HANDLER_LOG.debug("Sending reply [" + replyCode + replyTextToLog + "]");
153        session.sendReply(replyCode, text);
154    }
155
156    /**
157     * Return the specified text surrounded with double quotes
158     *
159     * @param text - the text to surround with quotes
160     * @return the text with leading and trailing double quotes
161     * @throws AssertFailedException - if text is null
162     */
163    protected static String quotes(String text) {
164        Assert.notNull(text, "text");
165        final String QUOTES = "\"";
166        return QUOTES + text + QUOTES;
167    }
168
169    /**
170     * Assert that the specified number is a valid reply code
171     *
172     * @param replyCode - the reply code to check
173     * @throws AssertFailedException - if the replyCode is invalid
174     */
175    protected void assertValidReplyCode(int replyCode) {
176        Assert.isTrue(replyCode > 0, "The number [" + replyCode + "] is not a valid reply code");
177    }
178
179    // -------------------------------------------------------------------------
180    // InvocationHistory - Support for command history
181    // -------------------------------------------------------------------------
182
183    /**
184     * @return the number of invocation records stored for this command handler instance
185     * @see org.mockftpserver.core.command.InvocationHistory#numberOfInvocations()
186     */
187    public int numberOfInvocations() {
188        return invocations.size();
189    }
190
191    /**
192     * Return the InvocationRecord representing the command invoction data for the nth invocation
193     * for this command handler instance. One InvocationRecord should be stored for each invocation
194     * of the CommandHandler.
195     *
196     * @param index - the index of the invocation record to return. The first record is at index zero.
197     * @return the InvocationRecord for the specified index
198     * @throws AssertFailedException - if there is no invocation record corresponding to the specified index
199     * @see org.mockftpserver.core.command.InvocationHistory#getInvocation(int)
200     */
201    public InvocationRecord getInvocation(int index) {
202        return (InvocationRecord) invocations.get(index);
203    }
204
205    /**
206     * Clear out the invocation history for this CommandHandler. After invoking this method, the
207     * <code>numberOfInvocations()</code> method will return zero.
208     *
209     * @see org.mockftpserver.core.command.InvocationHistory#clearInvocations()
210     */
211    public void clearInvocations() {
212        invocations.clear();
213    }
214
215    // -------------------------------------------------------------------------
216    // Internal Helper Methods
217    // -------------------------------------------------------------------------
218
219    /**
220     * Return the text for the specified reply code, formatted using the message arguments, if
221     * supplied. If overrideText is not null, then return that. Otherwise, return the text mapped to
222     * the code from the replyText ResourceBundle. If the ResourceBundle contains no mapping, then
223     * return null.
224     * <p/>
225     * If arguments is not null, then the returned reply text if formatted using the
226     * {@link MessageFormat} class.
227     *
228     * @param code         - the reply code
229     * @param messageKey   - the key used to retrieve the reply text from the replyTextBundle
230     * @param overrideText - if not null, this is used instead of the text from the replyTextBundle.
231     * @param arguments    - the array of arguments to be formatted and substituted within the reply
232     *                     text; may be null
233     * @return the text for the reply code; may be null
234     */
235    private String getTextForReplyCode(int code, String messageKey, String overrideText, Object[] arguments) {
236        try {
237            String t = (overrideText == null) ? replyTextBundle.getString(messageKey) : overrideText;
238            String formattedMessage = MessageFormat.format(t, arguments);
239            return (formattedMessage == null) ? null : formattedMessage.trim();
240        }
241        catch (MissingResourceException e) {
242            // No reply text is mapped for the specified key
243            LOG.warn("No reply text defined for reply code [" + code + "]");
244            return null;
245        }
246    }
247
248}
249