001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 *
019 */
020package org.apache.mina.filter.keepalive;
021
022import org.apache.mina.core.filterchain.IoFilter;
023import org.apache.mina.core.filterchain.IoFilterAdapter;
024import org.apache.mina.core.filterchain.IoFilterChain;
025import org.apache.mina.core.service.IoHandler;
026import org.apache.mina.core.session.AttributeKey;
027import org.apache.mina.core.session.IdleStatus;
028import org.apache.mina.core.session.IoEventType;
029import org.apache.mina.core.session.IoSession;
030import org.apache.mina.core.session.IoSessionConfig;
031import org.apache.mina.core.write.DefaultWriteRequest;
032import org.apache.mina.core.write.WriteRequest;
033
034/**
035 * An {@link IoFilter} that sends a keep-alive request on
036 * {@link IoEventType#SESSION_IDLE} and sends back the response for the
037 * sent keep-alive request.
038 *
039 * <h2>Interference with {@link IoSessionConfig#setIdleTime(IdleStatus, int)}</h2>
040 *
041 * This filter adjusts <tt>idleTime</tt> of the {@link IdleStatus}s that
042 * this filter is interested in automatically (e.g. {@link IdleStatus#READER_IDLE}
043 * and {@link IdleStatus#WRITER_IDLE}.)  Changing the <tt>idleTime</tt>
044 * of the {@link IdleStatus}s can lead this filter to a unexpected behavior.
045 * Please also note that any {@link IoFilter} and {@link IoHandler} behind
046 * {@link KeepAliveFilter} will not get any {@link IoEventType#SESSION_IDLE}
047 * event.  To receive the internal {@link IoEventType#SESSION_IDLE} event,
048 * you can call {@link #setForwardEvent(boolean)} with <tt>true</tt>.
049 *
050 * <h2>Implementing {@link KeepAliveMessageFactory}</h2>
051 *
052 * To use this filter, you have to provide an implementation of
053 * {@link KeepAliveMessageFactory}, which determines a received or sent
054 * message is a keep-alive message or not and creates a new keep-alive
055 * message:
056 *
057 * <table border="1" summary="Message">
058 *   <tr>
059 *     <th>Name</th><th>Description</th><th>Implementation</th>
060 *   </tr>
061 *   <tr valign="top">
062 *     <td>Active</td>
063 *     <td>
064 *       You want a keep-alive request is sent when the reader is idle.
065 *       Once the request is sent, the response for the request should be
066 *       received within <tt>keepAliveRequestTimeout</tt> seconds.  Otherwise,
067 *       the specified {@link KeepAliveRequestTimeoutHandler} will be invoked.
068 *       If a keep-alive request is received, its response also should be sent back.
069 *     </td>
070 *     <td>
071 *       Both {@link KeepAliveMessageFactory#getRequest(IoSession)} and
072 *       {@link KeepAliveMessageFactory#getResponse(IoSession, Object)} must
073 *       return a non-<tt>null</tt>.
074 *     </td>
075 *   </tr>
076 *   <tr valign="top">
077 *     <td>Semi-active</td>
078 *     <td>
079 *       You want a keep-alive request to be sent when the reader is idle.
080 *       However, you don't really care if the response is received or not.
081 *       If a keep-alive request is received, its response should
082 *       also be sent back.
083 *     </td>
084 *     <td>
085 *       Both {@link KeepAliveMessageFactory#getRequest(IoSession)} and
086 *       {@link KeepAliveMessageFactory#getResponse(IoSession, Object)} must
087 *       return a non-<tt>null</tt>, and the <tt>timeoutHandler</tt> property
088 *       should be set to {@link KeepAliveRequestTimeoutHandler#NOOP},
089 *       {@link KeepAliveRequestTimeoutHandler#LOG} or the custom {@link KeepAliveRequestTimeoutHandler}
090 *       implementation that doesn't affect the session state nor throw an exception.
091 *     </td>
092 *   </tr>
093 *   <tr valign="top">
094 *     <td>Passive</td>
095 *     <td>
096 *       You don't want to send a keep-alive request by yourself, but the
097 *       response should be sent back if a keep-alive request is received.
098 *     </td>
099 *     <td>
100 *       {@link KeepAliveMessageFactory#getRequest(IoSession)} must return
101 *       <tt>null</tt> and {@link KeepAliveMessageFactory#getResponse(IoSession, Object)}
102 *       must return a non-<tt>null</tt>.
103 *     </td>
104 *   </tr>
105 *   <tr valign="top">
106 *     <td>Deaf Speaker</td>
107 *     <td>
108 *       You want a keep-alive request to be sent when the reader is idle, but
109 *       you don't want to send any response back.
110 *     </td>
111 *     <td>
112 *       {@link KeepAliveMessageFactory#getRequest(IoSession)} must return
113 *       a non-<tt>null</tt>,
114 *       {@link KeepAliveMessageFactory#getResponse(IoSession, Object)} must
115 *       return <tt>null</tt> and the <tt>timeoutHandler</tt> must be set to
116 *       {@link KeepAliveRequestTimeoutHandler#DEAF_SPEAKER}.
117 *     </td>
118 *   </tr>
119 *   <tr valign="top">
120 *     <td>Silent Listener</td>
121 *     <td>
122 *       You don't want to send a keep-alive request by yourself nor send any
123 *       response back.
124 *     </td>
125 *     <td>
126 *       Both {@link KeepAliveMessageFactory#getRequest(IoSession)} and
127 *       {@link KeepAliveMessageFactory#getResponse(IoSession, Object)} must
128 *       return <tt>null</tt>.
129 *     </td>
130 *   </tr>
131 * </table>
132 * 
133 * Please note that you must implement
134 * {@link KeepAliveMessageFactory#isRequest(IoSession, Object)} and
135 * {@link KeepAliveMessageFactory#isResponse(IoSession, Object)} properly
136 * whatever case you chose.
137 *
138 * <h2>Handling timeout</h2>
139 *
140 * {@link KeepAliveFilter} will notify its {@link KeepAliveRequestTimeoutHandler}
141 * when {@link KeepAliveFilter} didn't receive the response message for a sent
142 * keep-alive message.  The default handler is {@link KeepAliveRequestTimeoutHandler#CLOSE},
143 * but you can use other presets such as {@link KeepAliveRequestTimeoutHandler#NOOP},
144 * {@link KeepAliveRequestTimeoutHandler#LOG} or {@link KeepAliveRequestTimeoutHandler#EXCEPTION}.
145 * You can even implement your own handler.
146 *
147 * <h3>Special handler: {@link KeepAliveRequestTimeoutHandler#DEAF_SPEAKER}</h3>
148 *
149 * {@link KeepAliveRequestTimeoutHandler#DEAF_SPEAKER} is a special handler which is
150 * dedicated for the 'deaf speaker' mode mentioned above.  Setting the
151 * <tt>timeoutHandler</tt> property to {@link KeepAliveRequestTimeoutHandler#DEAF_SPEAKER}
152 * stops this filter from waiting for response messages and therefore disables
153 * response timeout detection.
154 *
155 * @author <a href="http://mina.apache.org">Apache MINA Project</a>
156 * @org.apache.xbean.XBean
157 */
158public class KeepAliveFilter extends IoFilterAdapter {
159
160    private final AttributeKey WAITING_FOR_RESPONSE = new AttributeKey(getClass(), "waitingForResponse");
161
162    private final AttributeKey IGNORE_READER_IDLE_ONCE = new AttributeKey(getClass(), "ignoreReaderIdleOnce");
163
164    private final KeepAliveMessageFactory messageFactory;
165
166    private final IdleStatus interestedIdleStatus;
167
168    private volatile KeepAliveRequestTimeoutHandler requestTimeoutHandler;
169
170    private volatile int requestInterval;
171
172    private volatile int requestTimeout;
173
174    private volatile boolean forwardEvent;
175
176    /**
177     * Creates a new instance with the default properties.
178     * The default property values are:
179     * <ul>
180     * <li><tt>interestedIdleStatus</tt> - {@link IdleStatus#READER_IDLE}</li>
181     * <li><tt>policy</tt> = {@link KeepAliveRequestTimeoutHandler#CLOSE}</li>
182     * <li><tt>keepAliveRequestInterval</tt> - 60 (seconds)</li>
183     * <li><tt>keepAliveRequestTimeout</tt> - 30 (seconds)</li>
184     * </ul>
185     * 
186     * @param messageFactory The message factory to use 
187     */
188    public KeepAliveFilter(KeepAliveMessageFactory messageFactory) {
189        this(messageFactory, IdleStatus.READER_IDLE, KeepAliveRequestTimeoutHandler.CLOSE);
190    }
191
192    /**
193     * Creates a new instance with the default properties.
194     * The default property values are:
195     * <ul>
196     * <li><tt>policy</tt> = {@link KeepAliveRequestTimeoutHandler#CLOSE}</li>
197     * <li><tt>keepAliveRequestInterval</tt> - 60 (seconds)</li>
198     * <li><tt>keepAliveRequestTimeout</tt> - 30 (seconds)</li>
199     * </ul>
200     * 
201     * @param messageFactory The message factory to use 
202     * @param interestedIdleStatus The IdleStatus the filter is interested in
203     */
204    public KeepAliveFilter(KeepAliveMessageFactory messageFactory, IdleStatus interestedIdleStatus) {
205        this(messageFactory, interestedIdleStatus, KeepAliveRequestTimeoutHandler.CLOSE, 60, 30);
206    }
207
208    /**
209     * Creates a new instance with the default properties.
210     * The default property values are:
211     * <ul>
212     * <li><tt>interestedIdleStatus</tt> - {@link IdleStatus#READER_IDLE}</li>
213     * <li><tt>keepAliveRequestInterval</tt> - 60 (seconds)</li>
214     * <li><tt>keepAliveRequestTimeout</tt> - 30 (seconds)</li>
215     * </ul>
216     * 
217     * @param messageFactory The message factory to use 
218     * @param policy The TimeOut handler policy
219     */
220    public KeepAliveFilter(KeepAliveMessageFactory messageFactory, KeepAliveRequestTimeoutHandler policy) {
221        this(messageFactory, IdleStatus.READER_IDLE, policy, 60, 30);
222    }
223
224    /**
225     * Creates a new instance with the default properties.
226     * The default property values are:
227     * <ul>
228     * <li><tt>keepAliveRequestInterval</tt> - 60 (seconds)</li>
229     * <li><tt>keepAliveRequestTimeout</tt> - 30 (seconds)</li>
230     * </ul>
231     * 
232     * @param messageFactory The message factory to use 
233     * @param interestedIdleStatus The IdleStatus the filter is interested in
234     * @param policy The TimeOut handler policy
235     */
236    public KeepAliveFilter(KeepAliveMessageFactory messageFactory, IdleStatus interestedIdleStatus,
237            KeepAliveRequestTimeoutHandler policy) {
238        this(messageFactory, interestedIdleStatus, policy, 60, 30);
239    }
240
241    /**
242     * Creates a new instance.
243     * 
244     * @param messageFactory The message factory to use 
245     * @param interestedIdleStatus The IdleStatus the filter is interested in
246     * @param policy The TimeOut handler policy
247     * @param keepAliveRequestInterval the interval to use
248     * @param keepAliveRequestTimeout The timeout to use
249     */
250    public KeepAliveFilter(KeepAliveMessageFactory messageFactory, IdleStatus interestedIdleStatus,
251            KeepAliveRequestTimeoutHandler policy, int keepAliveRequestInterval, int keepAliveRequestTimeout) {
252        if (messageFactory == null) {
253            throw new IllegalArgumentException("messageFactory");
254        }
255        
256        if (interestedIdleStatus == null) {
257            throw new IllegalArgumentException("interestedIdleStatus");
258        }
259        
260        if (policy == null) {
261            throw new IllegalArgumentException("policy");
262        }
263
264        this.messageFactory = messageFactory;
265        this.interestedIdleStatus = interestedIdleStatus;
266        requestTimeoutHandler = policy;
267
268        setRequestInterval(keepAliveRequestInterval);
269        setRequestTimeout(keepAliveRequestTimeout);
270    }
271
272    /**
273     * @return The {@link IdleStatus} 
274     */
275    public IdleStatus getInterestedIdleStatus() {
276        return interestedIdleStatus;
277    }
278
279    /**
280     * @return The timeout request handler
281     */
282    public KeepAliveRequestTimeoutHandler getRequestTimeoutHandler() {
283        return requestTimeoutHandler;
284    }
285
286    /**
287     * Set the timeout handler
288     * 
289     * @param timeoutHandler The instance of {@link KeepAliveRequestTimeoutHandler} to use
290     */
291    public void setRequestTimeoutHandler(KeepAliveRequestTimeoutHandler timeoutHandler) {
292        if (timeoutHandler == null) {
293            throw new IllegalArgumentException("timeoutHandler");
294        }
295        requestTimeoutHandler = timeoutHandler;
296    }
297
298    /**
299     * @return the interval for keep alive messages
300     */
301    public int getRequestInterval() {
302        return requestInterval;
303    }
304
305    /**
306     * Sets the interval for keepAlive messages
307     * 
308     * @param keepAliveRequestInterval the interval to set
309     */
310    public void setRequestInterval(int keepAliveRequestInterval) {
311        if (keepAliveRequestInterval <= 0) {
312            throw new IllegalArgumentException("keepAliveRequestInterval must be a positive integer: "
313                    + keepAliveRequestInterval);
314        }
315        
316        requestInterval = keepAliveRequestInterval;
317    }
318
319    /**
320     * @return The timeout
321     */
322    public int getRequestTimeout() {
323        return requestTimeout;
324    }
325
326    /**
327     * Sets the timeout
328     * 
329     * @param keepAliveRequestTimeout The timeout to set
330     */
331    public void setRequestTimeout(int keepAliveRequestTimeout) {
332        if (keepAliveRequestTimeout <= 0) {
333            throw new IllegalArgumentException("keepAliveRequestTimeout must be a positive integer: "
334                    + keepAliveRequestTimeout);
335        }
336        
337        requestTimeout = keepAliveRequestTimeout;
338    }
339
340    /**
341     * @return The message factory
342     */
343    public KeepAliveMessageFactory getMessageFactory() {
344        return messageFactory;
345    }
346
347    /**
348     * @return <tt>true</tt> if and only if this filter forwards
349     * a {@link IoEventType#SESSION_IDLE} event to the next filter.
350     * By default, the value of this property is <tt>false</tt>.
351     */
352    public boolean isForwardEvent() {
353        return forwardEvent;
354    }
355
356    /**
357     * Sets if this filter needs to forward a
358     * {@link IoEventType#SESSION_IDLE} event to the next filter.
359     * By default, the value of this property is <tt>false</tt>.
360     * 
361     * @param forwardEvent a flag set to tell if the filter has to forward a {@link IoEventType#SESSION_IDLE} event
362     */
363    public void setForwardEvent(boolean forwardEvent) {
364        this.forwardEvent = forwardEvent;
365    }
366
367    /**
368     * {@inheritDoc}
369     */
370    @Override
371    public void onPreAdd(IoFilterChain parent, String name, NextFilter nextFilter) throws Exception {
372        if (parent.contains(this)) {
373            throw new IllegalArgumentException("You can't add the same filter instance more than once. "
374                    + "Create another instance and add it.");
375        }
376    }
377
378    /**
379     * {@inheritDoc}
380     */
381    @Override
382    public void onPostAdd(IoFilterChain parent, String name, NextFilter nextFilter) throws Exception {
383        resetStatus(parent.getSession());
384    }
385
386    /**
387     * {@inheritDoc}
388     */
389    @Override
390    public void onPostRemove(IoFilterChain parent, String name, NextFilter nextFilter) throws Exception {
391        resetStatus(parent.getSession());
392    }
393
394    /**
395     * {@inheritDoc}
396     */
397    @Override
398    public void messageReceived(NextFilter nextFilter, IoSession session, Object message) throws Exception {
399        try {
400            if (messageFactory.isRequest(session, message)) {
401                Object pongMessage = messageFactory.getResponse(session, message);
402
403                if (pongMessage != null) {
404                    nextFilter.filterWrite(session, new DefaultWriteRequest(pongMessage));
405                }
406            }
407
408            if (messageFactory.isResponse(session, message)) {
409                resetStatus(session);
410            }
411        } finally {
412            if (!isKeepAliveMessage(session, message)) {
413                nextFilter.messageReceived(session, message);
414            }
415        }
416    }
417
418    /**
419     * {@inheritDoc}
420     */
421    @Override
422    public void messageSent(NextFilter nextFilter, IoSession session, WriteRequest writeRequest) throws Exception {
423        Object message = writeRequest.getMessage();
424        
425        if (!isKeepAliveMessage(session, message)) {
426            nextFilter.messageSent(session, writeRequest);
427        }
428    }
429
430    /**
431     * {@inheritDoc}
432     */
433    @Override
434    public void sessionIdle(NextFilter nextFilter, IoSession session, IdleStatus status) throws Exception {
435        if (status == interestedIdleStatus) {
436            if (!session.containsAttribute(WAITING_FOR_RESPONSE)) {
437                Object pingMessage = messageFactory.getRequest(session);
438                
439                if (pingMessage != null) {
440                    nextFilter.filterWrite(session, new DefaultWriteRequest(pingMessage));
441
442                    // If policy is OFF, there's no need to wait for
443                    // the response.
444                    if (getRequestTimeoutHandler() != KeepAliveRequestTimeoutHandler.DEAF_SPEAKER) {
445                        markStatus(session);
446                        if (interestedIdleStatus == IdleStatus.BOTH_IDLE) {
447                            session.setAttribute(IGNORE_READER_IDLE_ONCE);
448                        }
449                    } else {
450                        resetStatus(session);
451                    }
452                }
453            } else {
454                handlePingTimeout(session);
455            }
456        } else if (status == IdleStatus.READER_IDLE) {
457            if (session.removeAttribute(IGNORE_READER_IDLE_ONCE) == null) {
458                if (session.containsAttribute(WAITING_FOR_RESPONSE)) {
459                    handlePingTimeout(session);
460                }
461            }
462        }
463
464        if (forwardEvent) {
465            nextFilter.sessionIdle(session, status);
466        }
467    }
468
469    private void handlePingTimeout(IoSession session) throws Exception {
470        resetStatus(session);
471        KeepAliveRequestTimeoutHandler handler = getRequestTimeoutHandler();
472        if (handler == KeepAliveRequestTimeoutHandler.DEAF_SPEAKER) {
473            return;
474        }
475
476        handler.keepAliveRequestTimedOut(this, session);
477    }
478
479    private void markStatus(IoSession session) {
480        session.getConfig().setIdleTime(interestedIdleStatus, 0);
481        session.getConfig().setReaderIdleTime(getRequestTimeout());
482        session.setAttribute(WAITING_FOR_RESPONSE);
483    }
484
485    private void resetStatus(IoSession session) {
486        session.getConfig().setReaderIdleTime(0);
487        session.getConfig().setWriterIdleTime(0);
488        session.getConfig().setIdleTime(interestedIdleStatus, getRequestInterval());
489        session.removeAttribute(WAITING_FOR_RESPONSE);
490    }
491
492    private boolean isKeepAliveMessage(IoSession session, Object message) {
493        return messageFactory.isRequest(session, message) || messageFactory.isResponse(session, message);
494    }
495}