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.mockftpserver.core.CommandSyntaxException;
19import org.mockftpserver.core.session.Session;
20import org.mockftpserver.core.util.Assert;
21import org.mockftpserver.core.util.AssertFailedException;
22
23import java.text.MessageFormat;
24import java.util.ArrayList;
25import java.util.List;
26import java.util.MissingResourceException;
27
28/**
29 * The abstract superclass for CommandHandler classes that manage the List of InvocationRecord
30 * objects corresponding to each invocation of the command handler, and provide helper methods for subclasses.
31 *
32 * @author Chris Mair
33 * @version $Revision$ - $Date$
34 */
35public abstract class AbstractTrackingCommandHandler extends AbstractCommandHandler implements InvocationHistory {
36
37    private List invocations = new ArrayList();
38
39    // -------------------------------------------------------------------------
40    // Template Method
41    // -------------------------------------------------------------------------
42
43    /**
44     * Handle the specified command for the session. This method is declared to throw Exception,
45     * allowing CommandHandler implementations to avoid unnecessary exception-handling. All checked
46     * exceptions are expected to be wrapped and handled by the caller.
47     *
48     * @param command - the Command to be handled
49     * @param session - the session on which the Command was submitted
50     * @throws Exception
51     * @throws AssertFailedException - if the command or session is null
52     * @see org.mockftpserver.core.command.CommandHandler#handleCommand(org.mockftpserver.core.command.Command,
53     *      org.mockftpserver.core.session.Session)
54     */
55    public final void handleCommand(Command command, Session session) throws Exception {
56        Assert.notNull(command, "command");
57        Assert.notNull(session, "session");
58        InvocationRecord invocationRecord = new InvocationRecord(command, session.getClientHost());
59        invocations.add(invocationRecord);
60        try {
61            handleCommand(command, session, invocationRecord);
62        }
63        catch (CommandSyntaxException e) {
64            sendReply(session, ReplyCodes.COMMAND_SYNTAX_ERROR, null, null, null);
65        }
66        invocationRecord.lock();
67    }
68
69    /**
70     * Handle the specified command for the session. This method is declared to throw Exception,
71     * allowing CommandHandler implementations to avoid unnecessary exception-handling. All checked
72     * exceptions are expected to be wrapped and handled by the caller.
73     *
74     * @param command          - the Command to be handled
75     * @param session          - the session on which the Command was submitted
76     * @param invocationRecord - the InvocationRecord; CommandHandlers are expected to add
77     *                         handler-specific data to the InvocationRecord, as appropriate
78     * @throws Exception
79     */
80    protected abstract void handleCommand(Command command, Session session, InvocationRecord invocationRecord)
81            throws Exception;
82
83    // -------------------------------------------------------------------------
84    // Utility methods for subclasses
85    // -------------------------------------------------------------------------
86
87    /**
88     * Send a reply for this command on the control connection.
89     * <p/>
90     * The reply code is designated by the <code>replyCode</code> property, and the reply text
91     * is determined by the following rules:
92     * <ol>
93     * <li>If the <code>replyText</code> property is non-null, then use that.</li>
94     * <li>Otherwise, if <code>replyMessageKey</code> is non-null, the use that to retrieve a
95     * localized message from the <code>replyText</code> ResourceBundle.</li>
96     * <li>Otherwise, retrieve the reply text from the <code>replyText</code> ResourceBundle,
97     * using the reply code as the key.</li>
98     * </ol>
99     * If the arguments Object[] is not null, then these arguments are substituted within the
100     * reply text using the {@link MessageFormat} class.
101     *
102     * @param session         - the Session
103     * @param replyCode       - the reply code
104     * @param replyMessageKey - if not null (and replyText is null), this is used as the ResourceBundle
105     *                        message key instead of the reply code.
106     * @param replyText       - if non-null, this is used as the reply text
107     * @param arguments       - the array of arguments to be formatted and substituted within the reply
108     *                        text; may be null
109     * @throws AssertFailedException - if session is null
110     * @see MessageFormat
111     */
112    protected void sendReply(Session session, int replyCode, String replyMessageKey, String replyText,
113                             Object[] arguments) {
114
115        Assert.notNull(session, "session");
116        assertValidReplyCode(replyCode);
117
118        String key = (replyMessageKey != null) ? replyMessageKey : Integer.toString(replyCode);
119        String text = getTextForReplyCode(replyCode, key, replyText, arguments);
120        String replyTextToLog = (text == null) ? "" : " " + text;
121        LOG.info("Sending reply [" + replyCode + replyTextToLog + "]");
122        session.sendReply(replyCode, text);
123    }
124
125    // -------------------------------------------------------------------------
126    // InvocationHistory - Support for command history
127    // -------------------------------------------------------------------------
128
129    /**
130     * @return the number of invocation records stored for this command handler instance
131     * @see org.mockftpserver.core.command.InvocationHistory#numberOfInvocations()
132     */
133    public int numberOfInvocations() {
134        return invocations.size();
135    }
136
137    /**
138     * Return the InvocationRecord representing the command invoction data for the nth invocation
139     * for this command handler instance. One InvocationRecord should be stored for each invocation
140     * of the CommandHandler.
141     *
142     * @param index - the index of the invocation record to return. The first record is at index zero.
143     * @return the InvocationRecord for the specified index
144     * @throws AssertFailedException - if there is no invocation record corresponding to the specified index
145     * @see org.mockftpserver.core.command.InvocationHistory#getInvocation(int)
146     */
147    public InvocationRecord getInvocation(int index) {
148        return (InvocationRecord) invocations.get(index);
149    }
150
151    /**
152     * Clear out the invocation history for this CommandHandler. After invoking this method, the
153     * <code>numberOfInvocations()</code> method will return zero.
154     *
155     * @see org.mockftpserver.core.command.InvocationHistory#clearInvocations()
156     */
157    public void clearInvocations() {
158        invocations.clear();
159    }
160
161    // -------------------------------------------------------------------------
162    // Internal Helper Methods
163    // -------------------------------------------------------------------------
164
165    /**
166     * Return the text for the specified reply code, formatted using the message arguments, if
167     * supplied. If overrideText is not null, then return that. Otherwise, return the text mapped to
168     * the code from the replyText ResourceBundle. If the ResourceBundle contains no mapping, then
169     * return null.
170     * <p/>
171     * If arguments is not null, then the returned reply text if formatted using the
172     * {@link MessageFormat} class.
173     *
174     * @param code         - the reply code
175     * @param messageKey   - the key used to retrieve the reply text from the replyTextBundle
176     * @param overrideText - if not null, this is used instead of the text from the replyTextBundle.
177     * @param arguments    - the array of arguments to be formatted and substituted within the reply
178     *                     text; may be null
179     * @return the text for the reply code; may be null
180     */
181    private String getTextForReplyCode(int code, String messageKey, String overrideText, Object[] arguments) {
182        try {
183            String t = (overrideText == null) ? getReplyTextBundle().getString(messageKey) : overrideText;
184            String formattedMessage = MessageFormat.format(t, arguments);
185            return (formattedMessage == null) ? null : formattedMessage.trim();
186        }
187        catch (MissingResourceException e) {
188            // No reply text is mapped for the specified key
189            LOG.warn("No reply text defined for reply code [" + code + "]");
190            return null;
191        }
192    }
193
194}
195