Java tutorial
/* * Version: 1.0 * * The contents of this file are subject to the OpenVPMS License Version * 1.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.openvpms.org/license/ * * Software distributed under the License is distributed on an 'AS IS' basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * Copyright 2015 (C) OpenVPMS Ltd. All Rights Reserved. */ package org.openvpms.web.echo.servlet; import nextapp.echo2.webrender.Connection; import nextapp.echo2.webrender.WebRenderServlet; import org.apache.commons.collections4.map.ReferenceMap; import org.apache.commons.lang.time.DateUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.openvpms.web.echo.spring.SpringApplicationInstance; import org.springframework.beans.factory.DisposableBean; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.lang.ref.WeakReference; import java.util.Collections; import java.util.Map; import java.util.WeakHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import static org.apache.commons.collections4.map.AbstractReferenceMap.ReferenceStrength.WEAK; /** * Monitors HTTP sessions, forcing idle sessions to expire. * <p/> * This is required as echo2 asynchronous tasks keep sessions alive, such that web.xml {@code <session-timeout/>} * has no effect. * * @author Tim Anderson */ public class SessionMonitor implements DisposableBean { /** * The default period before screen-lock, in minutes. */ public static final int DEFAULT_AUTO_LOCK_INTERVAL = 5; /** * The default session inactivity period before logout, in minutes. */ public static final int DEFAULT_AUTO_LOGOUT_INTERVAL = 30; /** * The time in milliseconds before inactive sessions have their screens locked. */ private volatile long autoLock = DEFAULT_AUTO_LOCK_INTERVAL * DateUtils.MILLIS_PER_MINUTE; /** * The time in milliseconds before inactive sessions are logged out. */ private volatile long autoLogout = DEFAULT_AUTO_LOGOUT_INTERVAL * DateUtils.MILLIS_PER_MINUTE; /** * The monitors, keyed on their sessions. */ private Map<HttpSession, Monitor> monitors = Collections .synchronizedMap(new WeakHashMap<HttpSession, Monitor>()); /** * The executor, used to expire sessions. */ private final ScheduledExecutorService executor; /** * The logger. */ private static final Log log = LogFactory.getLog(SessionMonitor.class); /** * Constructs an {@link SessionMonitor}. */ public SessionMonitor() { executor = Executors.newSingleThreadScheduledExecutor(); log.info("Using default session auto-lock time=" + (autoLock / DateUtils.MILLIS_PER_MINUTE) + " minutes"); log.info("Using default session auto-logout time=" + (autoLogout / DateUtils.MILLIS_PER_MINUTE) + " minutes"); } /** * Adds a session to monitor. * * @param session the session */ public void addSession(HttpSession session) { Monitor monitor = new Monitor(session); monitors.put(session, monitor); monitor.schedule(); } /** * Stops monitoring a session. * * @param session the session to remove */ public void removeSession(HttpSession session) { Monitor monitor = monitors.remove(session); if (monitor != null) { monitor.destroy(); } } /** * Marks the current session as active. */ public void active() { Connection connection = getConnection(); if (connection != null) { HttpServletRequest request = connection.getRequest(); active(request, getAuthentication()); } } /** * Marks a session as active. * * @param request the request * @param authentication the user authentication */ public void active(HttpServletRequest request, Authentication authentication) { Monitor monitor = monitors.get(request.getSession()); if (monitor != null) { monitor.active(request, authentication); } } /** * Determines if a session is locked. * * @param session the session * @return {@code true} if the session is locked */ public boolean isLocked(HttpSession session) { Monitor monitor = monitors.get(session); return monitor != null && monitor.locked; } /** * Registers a new application. * * @param application the new application * @param session the session that created the application */ public void newApplication(SpringApplicationInstance application, HttpSession session) { Monitor monitor = monitors.get(session); if (monitor != null) { monitor.newApplication(application); } } /** * Returns the time that sessions may remain idle before they are locked. * * @return the timeout, in minutes. A value of {@code 0} indicates that sessions don't lock */ public int getAutoLock() { return (int) (autoLock / DateUtils.MILLIS_PER_MINUTE); } /** * Sets the time that sessions may remain idle before they are locked. * * @param time the timeout, in minutes. A value of {@code 0} indicates that sessions don't lock */ public void setAutoLock(int time) { setAutoLockMS(time * DateUtils.MILLIS_PER_MINUTE); } /** * Unlocks the current session. */ public void unlock() { Connection connection = getConnection(); if (connection != null) { Monitor monitor = monitors.get(connection.getRequest().getSession()); if (monitor != null) { monitor.unlock(); } } } /** * Sets the time that sessions may remain idle before they are logged out. * * @param time the timeout, in minutes. A value of {@code 0} indicates that sessions don't expire */ public void setAutoLogout(int time) { setAutoLogoutMS(time * DateUtils.MILLIS_PER_MINUTE); } /** * Destroys this. */ @Override public void destroy() { log.info("Shutting down SessionMonitor"); executor.shutdown(); monitors.clear(); } /** * Returns the active connection. * * @return the active connection, or {@code null} if there is none */ protected Connection getConnection() { return WebRenderServlet.getActiveConnection(); } /** * Returns the authenticated user. * * @return the authenticated user. May be {@code null} */ protected Authentication getAuthentication() { return SecurityContextHolder.getContext().getAuthentication(); } /** * Sets the time that sessions may remain idle before they are locked. * * @param time the timeout, in milliseconds. A value of {@code 0} indicates that sessions don't lock */ protected void setAutoLockMS(long time) { if (time != autoLock) { autoLock = time; if (time == 0) { log.warn("Sessions configured to not auto-lock"); } else { log.info("Using session auto-lock time=" + (time / DateUtils.MILLIS_PER_MINUTE) + " minutes"); } reschedule(); } } /** * Sets the time that sessions may remain idle before they are logged out. * * @param time the timeout, in milliseconds. A value of {@code 0} indicates that sessions don't expire */ protected void setAutoLogoutMS(long time) { if (time != autoLogout) { autoLogout = time; if (time == 0) { log.warn("Sessions configured to not auto-logout"); } else { log.info("Using session auto-logout time=" + (time / DateUtils.MILLIS_PER_MINUTE) + " minutes"); } reschedule(); } } /** * Reschedule existing sessions. */ private void reschedule() { for (Object state : monitors.values().toArray()) { ((Monitor) state).reschedule(); } } /** * Monitors session activity. */ private class Monitor implements Runnable { /** * The session. */ private WeakReference<HttpSession> session; /** * Determines if the applications are currently locked. */ private volatile boolean locked; /** * The applications linked to the session. */ private final ReferenceMap<SpringApplicationInstance, SpringApplicationInstance> apps = new ReferenceMap<SpringApplicationInstance, SpringApplicationInstance>( WEAK, WEAK); /** * The time the session was last accessed, in milliseconds. */ private volatile long lastAccessedTime; /** * Used to cancel the scheduling. */ private volatile ScheduledFuture<?> future; /** * The user, used for logging. */ private volatile String user; /** * The user's IP address, used for logging. */ private volatile String address; /** * Constructs a {@link Monitor}. * * @param session the session */ public Monitor(HttpSession session) { lastAccessedTime = System.currentTimeMillis(); this.session = new WeakReference<HttpSession>(session); } /** * Marks the session as active. * * @param request the servlet request * @param authentication the user authentication */ public void active(HttpServletRequest request, Authentication authentication) { lastAccessedTime = System.currentTimeMillis(); boolean doLog = (user == null); Object principal = authentication.getPrincipal(); if (principal instanceof UserDetails) { user = ((UserDetails) principal).getUsername(); } else { user = principal.toString(); } address = request.getRemoteAddr(); if (doLog && log.isInfoEnabled()) { log.info("Active session, user=" + user + ", address=" + address); } } /** * Registers an application. * * @param application the application. */ public synchronized void newApplication(SpringApplicationInstance application) { apps.put(application, application); } /** * Invoked by the executor. * Delegates to {@link #monitor}. */ @Override public synchronized void run() { try { monitor(); } catch (Throwable exception) { log.error(exception, exception); } } /** * Schedules the monitor. */ public void schedule() { long time = autoLock; long logout = autoLogout; if (time == 0 || (logout != 0 && logout < time)) { time = logout; } if (time != 0) { schedule(time); } } /** * Reschedules the monitor. */ public void reschedule() { ScheduledFuture<?> current = future; if (current != null) { current.cancel(false); } long timeout = autoLock; if (timeout == 0) { timeout = autoLogout; } if (timeout != 0) { run(); // will either expire or schedule } } /** * Unlocks applications linked to the session. */ public synchronized void unlock() { try { SpringApplicationInstance[] list = apps.values() .toArray(new SpringApplicationInstance[apps.size()]); for (SpringApplicationInstance app : list) { if (app != null) { app.unlock(); } } } finally { locked = false; reschedule(); } if (log.isInfoEnabled()) { log.info("Unlocked session for user=" + user + ", address=" + address); } } /** * Destroys the monitor. */ public void destroy() { if (future != null) { future.cancel(true); } } /** * Checks the session activity. This: * <ul> * <li>invalidates the session if {@link #autoLogout} is non-zero and it hasn't been accessed for * {@link #autoLogout} milliseconds</li> * <li>locks the session if {@link #autoLock} is non-zero, and the session hasn't been accessed for * {@link #autoLock} milliseconds.</li> * </ul> * If it has been accessed, then it reschedules itself for another {@code autoLock} milliseconds. */ private void monitor() { long inactive = System.currentTimeMillis() - lastAccessedTime; long logout = autoLogout; long lock = autoLock; if (log.isDebugEnabled()) { log.debug("Monitor user=" + user + ", address=" + address + ", inactive=" + inactive + "ms, " + "logout=" + logout + "ms, lock=" + lock + "ms"); } if (logout != 0 && inactive >= logout) { // session is inactive, so kill it invalidate(); } else { long reschedule = (logout != 0) ? logout - inactive : 0; if (lock != 0 && lock < logout) { if (inactive >= lock) { if (!locked) { lock(); } } else { reschedule = lock - inactive; } } if (reschedule != 0) { schedule(reschedule); } } } /** * Schedules the monitor. * * @param delay the milliseconds to delay before invoking {@link #run}. */ private void schedule(long delay) { if (log.isDebugEnabled()) { log.info("Scheduling monitor for " + delay + "ms, user=" + user + ", address=" + address); } future = executor.schedule(this, delay, TimeUnit.MILLISECONDS); } /** * Locks applications linked to the session. */ private synchronized void lock() { locked = true; SpringApplicationInstance[] list = apps.values().toArray(new SpringApplicationInstance[apps.size()]); for (SpringApplicationInstance app : list) { if (app != null) { app.lock(); } } if (log.isInfoEnabled()) { log.info("Locked session for user=" + user + ", address=" + address); } } /** * Invalidates a session. */ private void invalidate() { HttpSession httpSession = session.get(); if (httpSession != null) { httpSession.invalidate(); if (log.isInfoEnabled()) { log.info("Invalidated session for user=" + user + ", address=" + address); } } } } }