diff --git a/java/javax/websocket/Endpoint.java b/java/javax/websocket/Endpoint.java index 9dfdbcce2b61..a387c403a09e 100644 --- a/java/javax/websocket/Endpoint.java +++ b/java/javax/websocket/Endpoint.java @@ -16,6 +16,8 @@ */ package javax.websocket; +import org.apache.tomcat.websocket.IdleStateEventType; + public abstract class Endpoint { /** @@ -46,4 +48,13 @@ public void onClose(Session session, CloseReason closeReason) { public void onError(Session session, Throwable throwable) { // NO-OP by default } + + /** + * Event that is triggered when a session is idle for more than maxIdleTime. + * @param session + * @param idleStateEventType + */ + public void onIdleSession(Session session, IdleStateEventType idleStateEventType) { + //NO-OP by default + } } diff --git a/java/javax/websocket/Session.java b/java/javax/websocket/Session.java index eea15e5be8ca..2790b70b5d05 100644 --- a/java/javax/websocket/Session.java +++ b/java/javax/websocket/Session.java @@ -77,6 +77,20 @@ public interface Session extends Closeable { */ void setMaxIdleTimeout(long timeout); + /** + * Get the flag to decide whether the session will be closed if idle Time is passed + * @return true if the session is to be closed on expire + * false if the session will remain open and an IdleSession event is thrown instead + */ + boolean getCloseOnIdleTimeout(); + + /** + * Get the flag to decide whether the session will be closed if idle Time is passed + * @param closeOnIdleTimeout- true for closing the session on idle timeout + * false for keeping the session open and throwing IdleSession event instead. + */ + void setCloseOnIdleTimeout(boolean closeOnIdleTimeout); + /** * Set the current maximum buffer size for binary messages. * @param max The new maximum buffer size in bytes diff --git a/java/org/apache/tomcat/websocket/IdleStateEventType.java b/java/org/apache/tomcat/websocket/IdleStateEventType.java new file mode 100644 index 000000000000..390e5f5eb0de --- /dev/null +++ b/java/org/apache/tomcat/websocket/IdleStateEventType.java @@ -0,0 +1,6 @@ +package org.apache.tomcat.websocket; + +public enum IdleStateEventType { + + IDLE_READ_EVENT, IDLE_WRITE_EVENT +} diff --git a/java/org/apache/tomcat/websocket/OnIdleSession.java b/java/org/apache/tomcat/websocket/OnIdleSession.java new file mode 100644 index 000000000000..b81245b8e02d --- /dev/null +++ b/java/org/apache/tomcat/websocket/OnIdleSession.java @@ -0,0 +1,12 @@ +package org.apache.tomcat.websocket; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface OnIdleSession { + +} diff --git a/java/org/apache/tomcat/websocket/WsFrameBase.java b/java/org/apache/tomcat/websocket/WsFrameBase.java index a6df700f60ce..863ac2fbb898 100644 --- a/java/org/apache/tomcat/websocket/WsFrameBase.java +++ b/java/org/apache/tomcat/websocket/WsFrameBase.java @@ -113,7 +113,7 @@ public WsFrameBase(WsSession wsSession, Transformation transformation) { protected void processInputBuffer() throws IOException { while (!isSuspended()) { - wsSession.updateLastActive(); + wsSession.updateLastReadTime(); if (state == State.NEW_FRAME) { if (!processInitialHeader()) { break; diff --git a/java/org/apache/tomcat/websocket/WsRemoteEndpointImplBase.java b/java/org/apache/tomcat/websocket/WsRemoteEndpointImplBase.java index c87f99eb49da..83f20af11ad6 100644 --- a/java/org/apache/tomcat/websocket/WsRemoteEndpointImplBase.java +++ b/java/org/apache/tomcat/websocket/WsRemoteEndpointImplBase.java @@ -275,7 +275,7 @@ private long getTimeoutExpiry() { private void sendMessageBlock(byte opCode, ByteBuffer payload, boolean last, long timeoutExpiry) throws IOException { - wsSession.updateLastActive(); + wsSession.updateLastWriteTime(); BlockingSendHandler bsh = new BlockingSendHandler(); @@ -340,7 +340,7 @@ private void sendMessageBlock(byte opCode, ByteBuffer payload, boolean last, void startMessage(byte opCode, ByteBuffer payload, boolean last, SendHandler handler) { - wsSession.updateLastActive(); + wsSession.updateLastWriteTime(); List messageParts = new ArrayList<>(); messageParts.add(new MessagePart(last, 0, opCode, payload, @@ -423,7 +423,7 @@ void endMessage(SendHandler handler, SendResult result) { writeMessagePart(mpNext); } - wsSession.updateLastActive(); + wsSession.updateLastWriteTime(); // Some handlers, such as the IntermediateMessageHandler, do not have a // nested handler so handler may be null. diff --git a/java/org/apache/tomcat/websocket/WsSession.java b/java/org/apache/tomcat/websocket/WsSession.java index 37bff3205922..d95b80259a82 100644 --- a/java/org/apache/tomcat/websocket/WsSession.java +++ b/java/org/apache/tomcat/websocket/WsSession.java @@ -97,7 +97,9 @@ public class WsSession implements Session { private volatile int maxBinaryMessageBufferSize = Constants.DEFAULT_BUFFER_SIZE; private volatile int maxTextMessageBufferSize = Constants.DEFAULT_BUFFER_SIZE; private volatile long maxIdleTimeout = 0; - private volatile long lastActive = System.currentTimeMillis(); + private volatile long lastWriteTime = System.currentTimeMillis(); + private volatile long lastReadTime = System.currentTimeMillis(); + private volatile boolean closeOnIdle = true; private Map futures = new ConcurrentHashMap<>(); /** @@ -385,6 +387,17 @@ public void setMaxIdleTimeout(long timeout) { this.maxIdleTimeout = timeout; } + @Override + public boolean getCloseOnIdleTimeout() { + checkState(); + return this.closeOnIdle; + } + + @Override + public void setCloseOnIdleTimeout(boolean closeOnIdleTimeout) { + checkState(); + this.closeOnIdle = closeOnIdleTimeout; + } @Override public void setMaxBinaryMessageBufferSize(int max) { @@ -575,8 +588,6 @@ private void fireEndpointOnClose(CloseReason closeReason) { } } - - private void fireEndpointOnError(Throwable throwable) { // Fire the onError event @@ -591,6 +602,10 @@ private void fireEndpointOnError(Throwable throwable) { } + private void fireEndpointOnIdle(IdleStateEventType idleStateEventType) { + localEndpoint.onIdleSession(this, idleStateEventType); + } + private void sendCloseMessage(CloseReason closeReason) { // 125 is maximum size for the payload of a control message ByteBuffer msg = ByteBuffer.allocate(125); @@ -805,24 +820,42 @@ protected MessageHandler.Whole getPongMessageHandler() { } - protected void updateLastActive() { - lastActive = System.currentTimeMillis(); + protected void updateLastWriteTime() { + lastWriteTime = System.currentTimeMillis(); } + protected void updateLastReadTime() { + lastReadTime = System.currentTimeMillis(); + } protected void checkExpiration() { long timeout = maxIdleTimeout; if (timeout < 1) { return; } + long currentTime = System.currentTimeMillis(); + boolean isWriteTimeout = currentTime - lastWriteTime > timeout; + boolean isReadTimeout = currentTime - lastReadTime > timeout; - if (System.currentTimeMillis() - lastActive > timeout) { - String msg = sm.getString("wsSession.timeout", getId()); - if (log.isDebugEnabled()) { - log.debug(msg); + if (isWriteTimeout || isReadTimeout) { + if (closeOnIdle) { + if (isWriteTimeout && isReadTimeout) { + String msg = sm.getString("wsSession.timeout", getId()); + if (log.isDebugEnabled()) { + log.debug(msg); + } + doClose(new CloseReason(CloseCodes.GOING_AWAY, msg), + new CloseReason(CloseCodes.CLOSED_ABNORMALLY, msg)); + } + } else { + String msg = sm.getString("wsSession.timeout", getId()); + if (log.isDebugEnabled()) { + log.debug(msg); + } + IdleStateEventType idleType = isWriteTimeout ? IdleStateEventType.IDLE_WRITE_EVENT : + IdleStateEventType.IDLE_READ_EVENT; + fireEndpointOnIdle(idleType); } - doClose(new CloseReason(CloseCodes.GOING_AWAY, msg), - new CloseReason(CloseCodes.CLOSED_ABNORMALLY, msg)); } } diff --git a/java/org/apache/tomcat/websocket/pojo/LocalStrings.properties b/java/org/apache/tomcat/websocket/pojo/LocalStrings.properties index bca550d06d59..4b29888fbeaa 100644 --- a/java/org/apache/tomcat/websocket/pojo/LocalStrings.properties +++ b/java/org/apache/tomcat/websocket/pojo/LocalStrings.properties @@ -18,6 +18,7 @@ pojoEndpointBase.onCloseFail=Failed to call onClose method of POJO end point for pojoEndpointBase.onError=No error handling configured for [{0}] and the following error occurred pojoEndpointBase.onErrorFail=Failed to call onError method of POJO end point for POJO of type [{0}] pojoEndpointBase.onOpenFail=Failed to call onOpen method of POJO end point for POJO of type [{0}] +pojoEndpointBase.onIdleSessionFail=Failed to call onIdleSession method of POJO end point for POJO of type [{0}] pojoEndpointServer.getPojoInstanceFail=Failed to create instance of POJO of type [{0}] diff --git a/java/org/apache/tomcat/websocket/pojo/PojoEndpointBase.java b/java/org/apache/tomcat/websocket/pojo/PojoEndpointBase.java index 75615d41c551..44ca4c0e708b 100644 --- a/java/org/apache/tomcat/websocket/pojo/PojoEndpointBase.java +++ b/java/org/apache/tomcat/websocket/pojo/PojoEndpointBase.java @@ -31,6 +31,7 @@ import org.apache.juli.logging.LogFactory; import org.apache.tomcat.util.ExceptionUtils; import org.apache.tomcat.util.res.StringManager; +import org.apache.tomcat.websocket.IdleStateEventType; /** * Base implementation (client and server have different concrete @@ -139,6 +140,21 @@ public final void onError(Session session, Throwable throwable) { } } + @Override + public void onIdleSession(Session session, IdleStateEventType idleStateEventType) { + + try { + methodMapping.getOnIdleSession().invoke( + pojo, + methodMapping.getOnIdleSessionArgs(pathParameters, session, + idleStateEventType)); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.error(sm.getString("pojoEndpointBase.onIdleSessionFail", + pojo.getClass().getName()), t); + } + } + protected Object getPojo() { return pojo; } protected void setPojo(Object pojo) { this.pojo = pojo; } diff --git a/java/org/apache/tomcat/websocket/pojo/PojoMethodMapping.java b/java/org/apache/tomcat/websocket/pojo/PojoMethodMapping.java index d640b5388278..b6ed1999db47 100644 --- a/java/org/apache/tomcat/websocket/pojo/PojoMethodMapping.java +++ b/java/org/apache/tomcat/websocket/pojo/PojoMethodMapping.java @@ -46,6 +46,8 @@ import org.apache.tomcat.util.res.StringManager; import org.apache.tomcat.websocket.DecoderEntry; +import org.apache.tomcat.websocket.IdleStateEventType; +import org.apache.tomcat.websocket.OnIdleSession; import org.apache.tomcat.websocket.Util; import org.apache.tomcat.websocket.Util.DecoderMatch; @@ -63,9 +65,11 @@ public class PojoMethodMapping { private final Method onOpen; private final Method onClose; private final Method onError; + private final Method onIdleSession; private final PojoPathParam[] onOpenParams; private final PojoPathParam[] onCloseParams; private final PojoPathParam[] onErrorParams; + private final PojoPathParam[] onIdleSessionParams; private final List onMessage = new ArrayList<>(); private final String wsPath; @@ -80,6 +84,7 @@ public PojoMethodMapping(Class clazzPojo, Method open = null; Method close = null; Method error = null; + Method idleSession = null; Method[] clazzPojoMethods = null; Class currentClazz = clazzPojo; while (!currentClazz.equals(Object.class)) { @@ -134,6 +139,19 @@ public PojoMethodMapping(Class clazzPojo, OnError.class, currentClazz)); } } + } else if (method.getAnnotation(OnIdleSession.class) != null) { + checkPublic(method); + if (idleSession == null) { + idleSession = method; + } else { + if (currentClazz == clazzPojo || + !isMethodOverride(idleSession, method)) { + // Duplicate annotation + throw new DeploymentException(sm.getString( + "pojoMethodMapping.duplicateAnnotation", + OnIdleSession.class, currentClazz)); + } + } } else if (method.getAnnotation(OnMessage.class) != null) { checkPublic(method); MessageHandlerInfo messageHandler = new MessageHandlerInfo(method, decoders); @@ -189,9 +207,11 @@ && isOverridenWithoutAnnotation(clazzPojoMethods, messageHandler.m, OnMessage.cl this.onOpen = open; this.onClose = close; this.onError = error; + this.onIdleSession = idleSession; onOpenParams = getPathParams(onOpen, MethodType.ON_OPEN); onCloseParams = getPathParams(onClose, MethodType.ON_CLOSE); onErrorParams = getPathParams(onError, MethodType.ON_ERROR); + onIdleSessionParams = getPathParams(onIdleSession, MethodType.ON_IDLE_SESSION); } @@ -235,7 +255,7 @@ public Method getOnOpen() { public Object[] getOnOpenArgs(Map pathParameters, Session session, EndpointConfig config) throws DecodeException { return buildArgs(onOpenParams, pathParameters, session, config, null, - null); + null, null); } @@ -247,21 +267,29 @@ public Method getOnClose() { public Object[] getOnCloseArgs(Map pathParameters, Session session, CloseReason closeReason) throws DecodeException { return buildArgs(onCloseParams, pathParameters, session, null, null, - closeReason); + closeReason, null); } public Method getOnError() { return onError; } - - + public Object[] getOnErrorArgs(Map pathParameters, Session session, Throwable throwable) throws DecodeException { return buildArgs(onErrorParams, pathParameters, session, null, - throwable, null); + throwable, null, null); } + public Method getOnIdleSession() { + return onIdleSession; + } + + public Object[] getOnIdleSessionArgs(Map pathParameters, + Session session, IdleStateEventType idleStateEventType) throws DecodeException { + return buildArgs(onIdleSessionParams, pathParameters, session, null, + null, null, idleStateEventType); + } public boolean hasMessageHandlers() { return !onMessage.isEmpty(); @@ -303,6 +331,9 @@ private static PojoPathParam[] getPathParams(Method m, } else if (methodType == MethodType.ON_CLOSE && type.equals(CloseReason.class)) { result[i] = new PojoPathParam(type, null); + } else if (methodType == MethodType.ON_IDLE_SESSION && + type.equals(IdleStateEventType.class)) { + result[i] = new PojoPathParam(type, null); } else { Annotation[] paramAnnotations = paramsAnnotations[i]; for (Annotation paramAnnotation : paramAnnotations) { @@ -332,7 +363,8 @@ private static PojoPathParam[] getPathParams(Method m, private static Object[] buildArgs(PojoPathParam[] pathParams, Map pathParameters, Session session, - EndpointConfig config, Throwable throwable, CloseReason closeReason) + EndpointConfig config, Throwable throwable, CloseReason closeReason, + IdleStateEventType IdleStateEventType) throws DecodeException { Object[] result = new Object[pathParams.length]; for (int i = 0; i < pathParams.length; i++) { @@ -345,7 +377,9 @@ private static Object[] buildArgs(PojoPathParam[] pathParams, result[i] = throwable; } else if (type.equals(CloseReason.class)) { result[i] = closeReason; - } else { + } else if(type.equals(IdleStateEventType.class)) { + result[i] = IdleStateEventType; + }else { String name = pathParams[i].getName(); String value = pathParameters.get(name); try { @@ -720,6 +754,7 @@ public Set getMessageHandlers(Object pojo, private enum MethodType { ON_OPEN, ON_CLOSE, - ON_ERROR + ON_ERROR, + ON_IDLE_SESSION } }