This is an automated email from the ASF dual-hosted git repository. markt pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/main by this push: new efa93c42c8 Implement HttpSession.getAccessor efa93c42c8 is described below commit efa93c42c8d9bb40486321b9e9339137d06b1259 Author: Mark Thomas <ma...@apache.org> AuthorDate: Wed Jan 10 14:17:35 2024 +0000 Implement HttpSession.getAccessor This will be included in the Servlet 6.1 API. --- java/jakarta/servlet/http/HttpSession.java | 36 ++++++ .../catalina/session/LocalStrings.properties | 7 ++ .../apache/catalina/session/StandardSession.java | 10 ++ .../catalina/session/StandardSessionAccessor.java | 79 ++++++++++++ .../catalina/session/StandardSessionFacade.java | 6 + .../session/TestStandardSessionAccessor.java | 138 +++++++++++++++++++++ webapps/docs/changelog.xml | 11 ++ 7 files changed, 287 insertions(+) diff --git a/java/jakarta/servlet/http/HttpSession.java b/java/jakarta/servlet/http/HttpSession.java index ecadd1e9bf..ccd055c8c5 100644 --- a/java/jakarta/servlet/http/HttpSession.java +++ b/java/jakarta/servlet/http/HttpSession.java @@ -17,6 +17,7 @@ package jakarta.servlet.http; import java.util.Enumeration; +import java.util.function.Consumer; import jakarta.servlet.ServletContext; @@ -51,6 +52,10 @@ import jakarta.servlet.ServletContext; * <p> * Session information is scoped only to the current web application ( <code>ServletContext</code>), so information * stored in one context will not be directly visible in another. + * <p> + * This object is <b>only</b> valid within the scope of the HTTP request from which it was obtained. Once the processing + * of that request returns to the container, this object must not be used. If there is a requirement to access the + * session outside of the scope of an HTTP request then this must be done via {@code #getAccessor()}. * * @see HttpSessionBindingListener */ @@ -194,4 +199,35 @@ public interface HttpSession { * @exception IllegalStateException if this method is called on an already invalidated session */ boolean isNew(); + + /** + * Provides a mechanism for applications to interact with the {@code HttpSession} outside of the scope of an HTTP + * request. + */ + interface Accessor { + /** + * Call to access the session with the same semantics as if the session was accessed during an HTTP request. + * <p> + * The effect of this call on the session is as if an HTTP request starts; {@link Consumer#accept(Object)} is + * called with the associated session object enabling the application to interact with the session; and, once + * that method returns, the HTTP request ends. + * + * @param sessionConsumer the application provided {@link Consumer} instance that will access the session + * + * @throws IllegalStateException if the session with the ID to which the {@link Accessor} is associated is no + * longer valid + */ + void access(Consumer<HttpSession> sessionConsumer); + } + + /** + * Provides a mechanism for applications to interact with the {@code HttpSession} outside of the scope of an HTTP + * request. + * + * @return An {@link Accessor} instance linked to the current session ID (if the session ID is changed the + * {@link Accessor} will no longer be able to access this session) + * + * @throws IllegalStateException if this method is called on an invalid session + */ + Accessor getAccessor(); } diff --git a/java/org/apache/catalina/session/LocalStrings.properties b/java/org/apache/catalina/session/LocalStrings.properties index eba297f8d9..6e8344ad02 100644 --- a/java/org/apache/catalina/session/LocalStrings.properties +++ b/java/org/apache/catalina/session/LocalStrings.properties @@ -73,6 +73,7 @@ standardManager.unloading.nosessions=No persisted sessions to unload standardSession.attributeEvent=Session attribute event listener threw exception standardSession.bindingEvent=Session binding event listener threw exception +standardSession.getAccessor.ise=getAccessor: Session already invalidated standardSession.getAttribute.ise=getAttribute: Session already invalidated standardSession.getAttributeNames.ise=getAttributeNames: Session already invalidated standardSession.getCreationTime.ise=getCreationTime: Session already invalidated @@ -92,3 +93,9 @@ standardSession.sessionEvent=Session event listener threw exception standardSession.setAttribute.iae=setAttribute: Non-serializable attribute [{0}] standardSession.setAttribute.ise=setAttribute: Session [{0}] has already been invalidated standardSession.setAttribute.namenull=setAttribute: name parameter cannot be null + +standardSessionAccessor.access.end=Unexpected error during the call to Session.endAccess() +standardSessionAccessor.access.invalid=Unable to access the session [{0}] as the session has already been invalidated +standardSessionAccessor.access.ioe=Unable to access the session [{0}] as an IOException occurred retrieving the session from the session manager +standardSessionAccessor.nullId=Unable to create Accessor instance as session ID is null +standardSessionAccessor.nullManager=Unable to create Accessor instance as session manager is null \ No newline at end of file diff --git a/java/org/apache/catalina/session/StandardSession.java b/java/org/apache/catalina/session/StandardSession.java index 8b63a7a020..81803a9d1c 100644 --- a/java/org/apache/catalina/session/StandardSession.java +++ b/java/org/apache/catalina/session/StandardSession.java @@ -621,6 +621,16 @@ public class StandardSession implements HttpSession, Session, Serializable { } + @Override + public Accessor getAccessor() { + if (!isValidInternal()) { + throw new IllegalStateException(sm.getString("standardSession.getAccessor.ise")); + } + + return new StandardSessionAccessor(getManager(), getId()); + } + + // ------------------------------------------------- Session Public Methods diff --git a/java/org/apache/catalina/session/StandardSessionAccessor.java b/java/org/apache/catalina/session/StandardSessionAccessor.java new file mode 100644 index 0000000000..c0730e83a3 --- /dev/null +++ b/java/org/apache/catalina/session/StandardSessionAccessor.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.catalina.session; + +import java.io.IOException; +import java.util.function.Consumer; + +import jakarta.servlet.http.HttpSession; + +import org.apache.catalina.Manager; +import org.apache.catalina.Session; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.res.StringManager; + +public class StandardSessionAccessor implements HttpSession.Accessor { + + private static final Log log = LogFactory.getLog(StandardSessionAccessor.class); + private static final StringManager sm = StringManager.getManager(StandardSessionAccessor.class); + + private final Manager manager; + private final String id; + + + public StandardSessionAccessor(Manager manager, String id) { + if (manager == null) { + throw new IllegalStateException(sm.getString("standardSessionAccessor.nullManager")); + } + if (id == null) { + throw new IllegalStateException(sm.getString("standardSessionAccessor.nullId")); + } + this.manager = manager; + this.id = id; + } + + + @Override + public void access(Consumer<HttpSession> sessionConsumer) { + + Session session; + try { + session = manager.findSession(id); + } catch (IOException e) { + throw new IllegalStateException(sm.getString("standardSessionAccessor.access.ioe", id)); + } + + if (session == null || !session.isValid()) { + // Should never be null but include it here just in case + throw new IllegalStateException(sm.getString("standardSessionAccessor.access.invalid", id)); + } + + session.access(); + try { + sessionConsumer.accept(session.getSession()); + } finally { + try { + session.endAccess(); + } catch (Throwable t) { + ExceptionUtils.handleThrowable(t); + log.warn(sm.getString("standardSessionAccessor.access.end"), t); + } + } + } +} diff --git a/java/org/apache/catalina/session/StandardSessionFacade.java b/java/org/apache/catalina/session/StandardSessionFacade.java index c0cc5c9f70..526c749c9e 100644 --- a/java/org/apache/catalina/session/StandardSessionFacade.java +++ b/java/org/apache/catalina/session/StandardSessionFacade.java @@ -120,4 +120,10 @@ public class StandardSessionFacade implements HttpSession { public boolean isNew() { return session.isNew(); } + + + @Override + public Accessor getAccessor() { + return session.getAccessor(); + } } diff --git a/test/org/apache/catalina/session/TestStandardSessionAccessor.java b/test/org/apache/catalina/session/TestStandardSessionAccessor.java new file mode 100644 index 0000000000..619a302d75 --- /dev/null +++ b/test/org/apache/catalina/session/TestStandardSessionAccessor.java @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.catalina.session; + +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.function.Consumer; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.HttpSession.Accessor; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Session; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; + +public class TestStandardSessionAccessor extends TomcatBaseTest { + + @Test + public void testLastAccess() throws Exception { + // Setup Tomcat instance + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + CountDownLatch latch = new CountDownLatch(1); + + Tomcat.addServlet(ctx, "accessor", new AccessorServlet(latch)); + ctx.addServletMappingDecoded("/accessor", "accessor"); + + tomcat.start(); + + int rc = getUrl("http://localhost:" + getPort() + "/accessor", null, null); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + // There should be a single session in the Manager at this point + Session[] sessions = ctx.getManager().findSessions(); + Assert.assertNotNull(sessions); + Assert.assertEquals(1, sessions.length); + + Session session = sessions[0]; + + // Check the current last accessed time + long lastAccessedA = session.getLastAccessedTime(); + + // Wait 1 second + Thread.sleep(1000); + + // Release the latch + latch.countDown(); + + // The last accessed time should have increased - allow up to 10 seconds for that to happen + int count = 0; + long lastAccessedB = session.getLastAccessedTime(); + while (lastAccessedB == lastAccessedA && count < 200) { + Thread.sleep(50); + lastAccessedB = session.getLastAccessedTime(); + count++; + } + + Assert.assertTrue("Session last access time not updated", lastAccessedB > lastAccessedA); + } + + + public static class AccessorServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final CountDownLatch latch; + + public AccessorServlet(CountDownLatch latch) { + this.latch = latch; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + HttpSession httpSession = req.getSession(); + Accessor acessor = httpSession.getAccessor(); + + Thread t = new Thread(new AccessorRunnable(acessor, latch)); + t.start(); + } + } + + + public static class AccessorRunnable implements Runnable { + + private final Accessor accessor; + private final CountDownLatch latch; + + public AccessorRunnable(Accessor accessor, CountDownLatch latch) { + this.accessor = accessor; + this.latch = latch; + } + + + @Override + public void run() { + try { + latch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + accessor.access(new SessionUpdater()); + } + } + + + public static class SessionUpdater implements Consumer<HttpSession> { + + @Override + public void accept(HttpSession httpSession) { + // NO-OP - process of accessing the session will update the last accessed time. + } + } +} diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index eb12e3d2d4..5732a67b37 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -105,6 +105,17 @@ issues do not "pop up" wrt. others). --> <section name="Tomcat 11.0.0-M17 (markt)" rtext="in development"> + <subsection name="Catalina"> + <changelog> + <add> + Implement <code>HttpSession.getAccessor()</code> which provides a + mechanism for applications to interact with an <code>HttpSession</code> + outside the standard Servlet processing of an HTTP request. This is + expected to be especially useful with applications using the Jakarta + WebSocket API. (markt) + </add> + </changelog> + </subsection> <subsection name="Coyote"> <changelog> <fix> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org