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