From b45e59192a4bb34a6b38a9bfa416b3dc3f6b7892 Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Sat, 8 Apr 2017 13:21:37 +0200 Subject: [PATCH] Improve CMS session. Introduce CMS sessions monitoring view. --- demo/log4j.properties | 2 +- .../ui/workbench/rap/RapWorkbenchLogin.java | 2 +- .../workbench/rap/SpnegoWorkbenchLogin.java | 2 +- org.argeo.cms.ui.workbench/plugin.xml | 13 +- .../ui/workbench/osgi/CmsSessionsView.java | 207 ++++++++++++++++++ .../argeo/cms/ui/AbstractCmsEntryPoint.java | 44 ++-- .../src/org/argeo/cms/ui/CmsView.java | 4 +- .../src/org/argeo/cms/util/CmsUtils.java | 4 +- .../org/argeo/cms/util/LoginEntryPoint.java | 57 +++-- .../org/argeo/cms/widgets/auth/CmsLogin.java | 13 +- .../argeo/cms/auth/AnonymousLoginModule.java | 4 +- .../src/org/argeo/cms/auth/CmsAuthUtils.java | 10 +- .../org/argeo/cms/auth/CmsAuthenticated.java | 11 + .../src/org/argeo/cms/auth/CmsSession.java | 29 ++- .../src/org/argeo/cms/auth/CurrentUser.java | 60 ++--- .../cms/auth/HttpSessionLoginModule.java | 2 +- .../argeo/cms/auth/UserAdminLoginModule.java | 4 +- .../cms/internal/auth/CmsSessionImpl.java | 68 +++++- .../cms/internal/http/CmsSessionProvider.java | 55 ++--- .../argeo/cms/internal/http/HttpFilter.java | 40 ---- .../cms/internal/http/WebCmsSessionImpl.java | 27 ++- .../cms/internal/kernel/KernelThread.java | 4 + .../argeo/jcr/proxy/ResourceProxyServlet.java | 2 +- .../node/security/NodeAuthenticated.java | 11 - .../src/org/argeo/util/LangUtils.java | 27 +++ 25 files changed, 510 insertions(+), 192 deletions(-) create mode 100644 org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/osgi/CmsSessionsView.java create mode 100644 org.argeo.cms/src/org/argeo/cms/auth/CmsAuthenticated.java delete mode 100644 org.argeo.cms/src/org/argeo/cms/internal/http/HttpFilter.java delete mode 100644 org.argeo.node.api/src/org/argeo/node/security/NodeAuthenticated.java diff --git a/demo/log4j.properties b/demo/log4j.properties index 2fd64cd2a..01027a532 100644 --- a/demo/log4j.properties +++ b/demo/log4j.properties @@ -1,4 +1,4 @@ -log4j.rootLogger=WARN, console +log4j.rootLogger=WARN, development log4j.logger.org.argeo=DEBUG log4j.logger.org.argeo.cms.internal=TRACE diff --git a/org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/RapWorkbenchLogin.java b/org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/RapWorkbenchLogin.java index a023013a8..73aac8282 100644 --- a/org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/RapWorkbenchLogin.java +++ b/org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/RapWorkbenchLogin.java @@ -44,7 +44,7 @@ public class RapWorkbenchLogin extends LoginEntryPoint { @Override protected int postLogin() { - Subject subject = getLoginContext().getSubject(); + Subject subject = getSubject(); final Display display = Display.getCurrent(); if (subject.getPrincipals(X500Principal.class).isEmpty()) { RWT.getClient().getService(JavaScriptExecutor.class).execute("location.reload()"); diff --git a/org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/SpnegoWorkbenchLogin.java b/org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/SpnegoWorkbenchLogin.java index efb251828..2f4ef095b 100644 --- a/org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/SpnegoWorkbenchLogin.java +++ b/org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/SpnegoWorkbenchLogin.java @@ -53,7 +53,7 @@ public class SpnegoWorkbenchLogin extends LoginEntryPoint { @Override protected int postLogin() { - Subject subject = getLoginContext().getSubject(); + Subject subject = getSubject(); final Display display = Display.getCurrent(); if (subject.getPrincipals(X500Principal.class).isEmpty()) { RWT.getClient().getService(JavaScriptExecutor.class).execute("location.reload()"); diff --git a/org.argeo.cms.ui.workbench/plugin.xml b/org.argeo.cms.ui.workbench/plugin.xml index b28859fdf..c656c2ff9 100644 --- a/org.argeo.cms.ui.workbench/plugin.xml +++ b/org.argeo.cms.ui.workbench/plugin.xml @@ -39,12 +39,17 @@ + + + (column, new Comparator() { + public int compare(CmsSession o1, CmsSession o2) { + return o1.getAuthorization().toString().compareTo(o2.getAuthorization().toString()); + } + }); + + // Creation time + column = new TableViewerColumn(viewer, SWT.NONE); + column.getColumn().setWidth(smallColWidth); + column.getColumn().setText("Since"); + column.setLabelProvider(new ColumnLabelProvider() { + private static final long serialVersionUID = -5234573509093747505L; + + public String getText(Object element) { + return LangUtils.since(((CmsSession) element).getCreationTime()); + } + + public String getToolTipText(Object element) { + return ((CmsSession) element).getCreationTime().toString(); + } + }); + new ColumnViewerComparator(column, new Comparator() { + public int compare(CmsSession o1, CmsSession o2) { + return o1.getCreationTime().compareTo(o2.getCreationTime()); + } + }); + + // Username + column = new TableViewerColumn(viewer, SWT.NONE); + column.getColumn().setWidth(smallColWidth); + column.getColumn().setText("Username"); + column.setLabelProvider(new ColumnLabelProvider() { + private static final long serialVersionUID = -5234573509093747505L; + + public String getText(Object element) { + LdapName userDn = ((CmsSession) element).getUserDn(); + return userDn.getRdn(userDn.size() - 1).getValue().toString(); + } + + public String getToolTipText(Object element) { + return ((CmsSession) element).getUserDn().toString(); + } + }); + new ColumnViewerComparator(column, new Comparator() { + public int compare(CmsSession o1, CmsSession o2) { + return o1.getUserDn().compareTo(o2.getUserDn()); + } + }); + + // UUID + column = new TableViewerColumn(viewer, SWT.NONE); + column.getColumn().setWidth(smallColWidth); + column.getColumn().setText("UUID"); + column.setLabelProvider(new ColumnLabelProvider() { + private static final long serialVersionUID = -5234573509093747505L; + + public String getText(Object element) { + return ((CmsSession) element).getUuid().toString(); + } + + public String getToolTipText(Object element) { + return getText(element); + } + }); + new ColumnViewerComparator(column, new Comparator() { + public int compare(CmsSession o1, CmsSession o2) { + return o1.getUuid().compareTo(o2.getUuid()); + } + }); + + // Local ID + column = new TableViewerColumn(viewer, SWT.NONE); + column.getColumn().setWidth(smallColWidth); + column.getColumn().setText("Local ID"); + column.setLabelProvider(new ColumnLabelProvider() { + private static final long serialVersionUID = -5234573509093747505L; + + public String getText(Object element) { + return ((CmsSession) element).getLocalId(); + } + + public String getToolTipText(Object element) { + return getText(element); + } + }); + new ColumnViewerComparator(column, new Comparator() { + public int compare(CmsSession o1, CmsSession o2) { + return o1.getLocalId().compareTo(o2.getLocalId()); + } + }); + + viewer.setInput(WorkbenchUiPlugin.getDefault().getBundle().getBundleContext()); + + } + + @Override + public void setFocus() { + if (viewer != null) + viewer.getControl().setFocus(); + } + + /** Content provider managing the array of bundles */ + private static class CmsSessionContentProvider implements IStructuredContentProvider { + private static final long serialVersionUID = -8533792785725875977L; + + public Object[] getElements(Object inputElement) { + if (inputElement instanceof BundleContext) { + BundleContext bc = (BundleContext) inputElement; + Collection> srs; + try { + srs = bc.getServiceReferences(CmsSession.class, null); + } catch (InvalidSyntaxException e) { + throw new CmsException("Cannot retrieve CMS sessions", e); + } + List res = new ArrayList<>(); + for (ServiceReference sr : srs) { + res.add(bc.getService(sr)); + } + return res.toArray(); + } + return null; + } + + public void dispose() { + } + + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + } +} diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/AbstractCmsEntryPoint.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/AbstractCmsEntryPoint.java index bea7117a6..99989db19 100644 --- a/org.argeo.cms.ui/src/org/argeo/cms/ui/AbstractCmsEntryPoint.java +++ b/org.argeo.cms.ui/src/org/argeo/cms/ui/AbstractCmsEntryPoint.java @@ -21,10 +21,10 @@ import org.apache.commons.logging.LogFactory; import org.argeo.cms.CmsException; import org.argeo.cms.auth.CurrentUser; import org.argeo.cms.auth.HttpRequestCallbackHandler; +import org.argeo.cms.auth.CmsAuthenticated; import org.argeo.eclipse.ui.specific.UiContext; import org.argeo.jcr.JcrUtils; import org.argeo.node.NodeConstants; -import org.argeo.node.security.NodeAuthenticated; import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.application.AbstractEntryPoint; import org.eclipse.rap.rwt.client.WebClient; @@ -71,19 +71,20 @@ public abstract class AbstractCmsEntryPoint extends AbstractEntryPoint implement // subject = new Subject(); // Initial login + LoginContext lc; try { - loginContext = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, + lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, new HttpRequestCallbackHandler(UiContext.getHttpRequest(), UiContext.getHttpResponse())); - loginContext.login(); + lc.login(); } catch (LoginException e) { try { - loginContext = new LoginContext(NodeConstants.LOGIN_CONTEXT_ANONYMOUS); - loginContext.login(); + lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_ANONYMOUS); + lc.login(); } catch (LoginException e1) { throw new CmsException("Cannot log in as anonymous", e1); } } - authChange(loginContext); + authChange(lc); jsExecutor = RWT.getClient().getService(JavaScriptExecutor.class); browserNavigation = RWT.getClient().getService(BrowserNavigation.class); @@ -109,8 +110,8 @@ public abstract class AbstractCmsEntryPoint extends AbstractEntryPoint implement @Override protected final void createContents(final Composite parent) { - UiContext.setData(NodeAuthenticated.KEY, this); - Subject.doAs(loginContext.getSubject(), new PrivilegedAction() { + UiContext.setData(CmsAuthenticated.KEY, this); + Subject.doAs(getSubject(), new PrivilegedAction() { @Override public Void run() { try { @@ -162,9 +163,12 @@ public abstract class AbstractCmsEntryPoint extends AbstractEntryPoint implement // return subject; // } - @Override - public LoginContext getLoginContext() { - return loginContext; + // @Override + // public LoginContext getLoginContext() { + // return loginContext; + // } + public Subject getSubject() { + return loginContext.getSubject(); } @Override @@ -183,11 +187,18 @@ public abstract class AbstractCmsEntryPoint extends AbstractEntryPoint implement } @Override - public synchronized void authChange(LoginContext loginContext) { - if (loginContext == null) + public synchronized void authChange(LoginContext lc) { + if (lc == null) throw new CmsException("Login context cannot be null"); - this.loginContext = loginContext; - Subject.doAs(loginContext.getSubject(), new PrivilegedAction() { + // logout previous login context + if (this.loginContext != null) + try { + this.loginContext.logout(); + } catch (LoginException e1) { + log.warn("Could not log out: " + e1); + } + this.loginContext = lc; + Subject.doAs(getSubject(), new PrivilegedAction() { @Override public Void run() { @@ -210,7 +221,6 @@ public abstract class AbstractCmsEntryPoint extends AbstractEntryPoint implement } }); - } @Override @@ -221,7 +231,7 @@ public abstract class AbstractCmsEntryPoint extends AbstractEntryPoint implement } protected synchronized void doRefresh() { - Subject.doAs(loginContext.getSubject(), new PrivilegedAction() { + Subject.doAs(getSubject(), new PrivilegedAction() { @Override public Void run() { refresh(); diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/CmsView.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/CmsView.java index d39d6df16..51f6acc8c 100644 --- a/org.argeo.cms.ui/src/org/argeo/cms/ui/CmsView.java +++ b/org.argeo.cms.ui/src/org/argeo/cms/ui/CmsView.java @@ -2,10 +2,10 @@ package org.argeo.cms.ui; import javax.security.auth.login.LoginContext; -import org.argeo.node.security.NodeAuthenticated; +import org.argeo.cms.auth.CmsAuthenticated; /** Provides interaction with the CMS system. UNSTABLE API at this stage. */ -public interface CmsView extends NodeAuthenticated { +public interface CmsView extends CmsAuthenticated { UxContext getUxContext(); // NAVIGATION diff --git a/org.argeo.cms.ui/src/org/argeo/cms/util/CmsUtils.java b/org.argeo.cms.ui/src/org/argeo/cms/util/CmsUtils.java index 7864dde8b..3bb984b6a 100644 --- a/org.argeo.cms.ui/src/org/argeo/cms/util/CmsUtils.java +++ b/org.argeo.cms.ui/src/org/argeo/cms/util/CmsUtils.java @@ -10,13 +10,13 @@ import javax.servlet.http.HttpServletRequest; import org.apache.commons.io.IOUtils; import org.argeo.cms.CmsException; +import org.argeo.cms.auth.CmsAuthenticated; import org.argeo.cms.ui.CmsConstants; import org.argeo.cms.ui.CmsView; import org.argeo.eclipse.ui.specific.UiContext; import org.argeo.jcr.JcrUtils; import org.argeo.node.NodeConstants; import org.argeo.node.NodeUtils; -import org.argeo.node.security.NodeAuthenticated; import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.service.ResourceManager; import org.eclipse.swt.SWT; @@ -41,7 +41,7 @@ public class CmsUtils implements CmsConstants { * this call. */ public static CmsView getCmsView() { - return UiContext.getData(NodeAuthenticated.KEY); + return UiContext.getData(CmsAuthenticated.KEY); } public static StringBuilder getServerBaseUrl(HttpServletRequest request) { diff --git a/org.argeo.cms.ui/src/org/argeo/cms/util/LoginEntryPoint.java b/org.argeo.cms.ui/src/org/argeo/cms/util/LoginEntryPoint.java index a8b52a5c2..a43b9ee92 100644 --- a/org.argeo.cms.ui/src/org/argeo/cms/util/LoginEntryPoint.java +++ b/org.argeo.cms.ui/src/org/argeo/cms/util/LoginEntryPoint.java @@ -2,12 +2,16 @@ package org.argeo.cms.util; import java.util.Locale; +import javax.security.auth.Subject; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import javax.servlet.http.HttpServletRequest; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.argeo.cms.CmsException; import org.argeo.cms.auth.CurrentUser; +import org.argeo.cms.auth.CmsAuthenticated; import org.argeo.cms.ui.CmsImageManager; import org.argeo.cms.ui.CmsView; import org.argeo.cms.ui.UxContext; @@ -15,7 +19,6 @@ import org.argeo.cms.widgets.auth.CmsLogin; import org.argeo.cms.widgets.auth.CmsLoginShell; import org.argeo.eclipse.ui.specific.UiContext; import org.argeo.node.NodeConstants; -import org.argeo.node.security.NodeAuthenticated; import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.application.EntryPoint; import org.eclipse.swt.events.SelectionListener; @@ -25,15 +28,14 @@ import org.eclipse.swt.widgets.Display; public class LoginEntryPoint implements EntryPoint, CmsView { protected final static String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate"; protected final static String HEADER_AUTHORIZATION = "Authorization"; - // private final static Log log = LogFactory.getLog(WorkbenchLogin.class); - // private final Subject subject = new Subject(); + private final static Log log = LogFactory.getLog(LoginEntryPoint.class); private LoginContext loginContext; private UxContext uxContext = null; @Override public int createUI() { final Display display = createDisplay(); - UiContext.setData(NodeAuthenticated.KEY, this); + UiContext.setData(CmsAuthenticated.KEY, this); CmsLoginShell loginShell = createCmsLoginShell(); try { // try pre-auth @@ -43,19 +45,21 @@ public class LoginEntryPoint implements EntryPoint, CmsView { loginShell.createUi(); loginShell.open(); -// HttpServletRequest request = RWT.getRequest(); -// String authorization = request.getHeader(HEADER_AUTHORIZATION); -// if (authorization == null || !authorization.startsWith("Negotiate")) { -// HttpServletResponse response = RWT.getResponse(); -// response.setStatus(401); -// response.setHeader(HEADER_WWW_AUTHENTICATE, "Negotiate"); -// response.setDateHeader("Date", System.currentTimeMillis()); -// response.setDateHeader("Expires", System.currentTimeMillis() + (24 * 60 * 60 * 1000)); -// response.setHeader("Accept-Ranges", "bytes"); -// response.setHeader("Connection", "Keep-Alive"); -// response.setHeader("Keep-Alive", "timeout=5, max=97"); -// // response.setContentType("text/html; charset=UTF-8"); -// } + // HttpServletRequest request = RWT.getRequest(); + // String authorization = request.getHeader(HEADER_AUTHORIZATION); + // if (authorization == null || + // !authorization.startsWith("Negotiate")) { + // HttpServletResponse response = RWT.getResponse(); + // response.setStatus(401); + // response.setHeader(HEADER_WWW_AUTHENTICATE, "Negotiate"); + // response.setDateHeader("Date", System.currentTimeMillis()); + // response.setDateHeader("Expires", System.currentTimeMillis() + + // (24 * 60 * 60 * 1000)); + // response.setHeader("Accept-Ranges", "bytes"); + // response.setHeader("Connection", "Keep-Alive"); + // response.setHeader("Keep-Alive", "timeout=5, max=97"); + // // response.setContentType("text/html; charset=UTF-8"); + // } while (!loginShell.getShell().isDisposed()) { if (!display.readAndDispatch()) @@ -121,6 +125,15 @@ public class LoginEntryPoint implements EntryPoint, CmsView { @Override public void authChange(LoginContext loginContext) { + if (loginContext == null) + throw new CmsException("Login context cannot be null"); + // logout previous login context + if (this.loginContext != null) + try { + this.loginContext.logout(); + } catch (LoginException e1) { + log.warn("Could not log out: " + e1); + } this.loginContext = loginContext; } @@ -142,9 +155,13 @@ public class LoginEntryPoint implements EntryPoint, CmsView { } - @Override - public LoginContext getLoginContext() { - return loginContext; + // @Override + // public LoginContext getLoginContext() { + // return loginContext; + // } + + public Subject getSubject() { + return loginContext.getSubject(); } @Override diff --git a/org.argeo.cms.ui/src/org/argeo/cms/widgets/auth/CmsLogin.java b/org.argeo.cms.ui/src/org/argeo/cms/widgets/auth/CmsLogin.java index 76263c1f2..f25ad19e2 100644 --- a/org.argeo.cms.ui/src/org/argeo/cms/widgets/auth/CmsLogin.java +++ b/org.argeo.cms.ui/src/org/argeo/cms/widgets/auth/CmsLogin.java @@ -13,7 +13,6 @@ import javax.security.auth.callback.LanguageCallback; import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.PasswordCallback; import javax.security.auth.callback.UnsupportedCallbackException; -import javax.security.auth.login.FailedLoginException; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; @@ -80,7 +79,7 @@ public class CmsLogin implements CmsStyles, CallbackHandler { } protected boolean isAnonymous() { - return CurrentUser.isAnonymous(cmsView.getLoginContext().getSubject()); + return CurrentUser.isAnonymous(cmsView.getSubject()); } public final void createUi(Composite parent) { @@ -247,14 +246,16 @@ public class CmsLogin implements CmsStyles, CallbackHandler { protected boolean login() { // Subject subject = cmsView.getLoginContext().getSubject(); - LoginContext loginContext = cmsView.getLoginContext(); +// LoginContext loginContext = cmsView.getLoginContext(); try { // // LOGIN // - loginContext.logout(); - loginContext = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, this); +// loginContext.logout(); + LoginContext loginContext = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, this); loginContext.login(); + cmsView.authChange(loginContext); + return true; } catch (LoginException e) { if (log.isTraceEnabled()) log.warn("Login failed: " + e.getMessage(), e); @@ -273,8 +274,6 @@ public class CmsLogin implements CmsStyles, CallbackHandler { // log.error("Cannot login", e); // return false; // } - cmsView.authChange(loginContext); - return true; } protected void logout() { diff --git a/org.argeo.cms/src/org/argeo/cms/auth/AnonymousLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/AnonymousLoginModule.java index 12a070415..7a1283afa 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/AnonymousLoginModule.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/AnonymousLoginModule.java @@ -64,8 +64,8 @@ public class AnonymousLoginModule implements LoginModule { @Override public boolean logout() throws LoginException { - if (log.isDebugEnabled()) - log.debug("Logging out anonymous from CMS... " + subject); + if (log.isTraceEnabled()) + log.trace("Logging out anonymous from CMS... " + subject); CmsAuthUtils.cleanUp(subject); return true; } diff --git a/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java b/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java index 4d59c5263..33a8dc62e 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java @@ -118,19 +118,19 @@ class CmsAuthUtils { private static void registerSessionAuthorization(HttpServletRequest request, Subject subject, Authorization authorization) { if (request != null) { - HttpSession httpSession = request.getSession(); + HttpSession httpSession = request.getSession(false); String httpSessId = httpSession.getId(); String remoteUser = authorization.getName() != null ? authorization.getName() : NodeConstants.ROLE_ANONYMOUS; request.setAttribute(HttpContext.REMOTE_USER, remoteUser); request.setAttribute(HttpContext.AUTHORIZATION, authorization); - CmsSession cmsSession = CmsSessionImpl.getByLocalId(httpSessId); + CmsSessionImpl cmsSession = (CmsSessionImpl) CmsSessionImpl.getByLocalId(httpSessId); if (cmsSession != null) { if (authorization.getName() != null) { if (cmsSession.getAuthorization().getName() == null) { // FIXME make it more generic - ((WebCmsSessionImpl) cmsSession).cleanUp(); + cmsSession.close(); cmsSession = null; } else if (!authorization.getName().equals(cmsSession.getAuthorization().getName())) { throw new CmsException("Inconsistent user " + authorization.getName() @@ -139,14 +139,14 @@ class CmsAuthUtils { } else {// anonymous if (cmsSession.getAuthorization().getName() != null) { // FIXME make it more generic - ((WebCmsSessionImpl) cmsSession).cleanUp(); + cmsSession.close(); cmsSession = null; } } } if (cmsSession == null) - cmsSession = new WebCmsSessionImpl(subject, authorization, httpSessId); + cmsSession = new WebCmsSessionImpl(subject, authorization, request); // request.setAttribute(CmsSession.class.getName(), cmsSession); CmsSessionId nodeSessionId = new CmsSessionId(cmsSession.getUuid()); if (subject.getPrivateCredentials(CmsSessionId.class).size() == 0) diff --git a/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthenticated.java b/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthenticated.java new file mode 100644 index 000000000..704e9335e --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthenticated.java @@ -0,0 +1,11 @@ +package org.argeo.cms.auth; + +import javax.security.auth.Subject; + +public interface CmsAuthenticated { + String KEY = "org.argeo.cms.authenticated"; + + Subject getSubject(); +// LoginContext getLoginContext(); + +} diff --git a/org.argeo.cms/src/org/argeo/cms/auth/CmsSession.java b/org.argeo.cms/src/org/argeo/cms/auth/CmsSession.java index 14d6d71f6..118f87589 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/CmsSession.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/CmsSession.java @@ -1,32 +1,39 @@ package org.argeo.cms.auth; +import java.time.ZonedDateTime; import java.util.UUID; -import javax.jcr.Repository; -import javax.jcr.Session; import javax.naming.ldap.LdapName; import org.argeo.naming.LdapAttrs; import org.osgi.service.useradmin.Authorization; public interface CmsSession { - public final static String USER_DN = LdapAttrs.DN; - public final static String SESSION_UUID = LdapAttrs.entryUUID.name(); - public final static String SESSION_LOCAL_ID = LdapAttrs.uniqueIdentifier.name(); + final static String USER_DN = LdapAttrs.DN; + final static String SESSION_UUID = LdapAttrs.entryUUID.name(); + final static String SESSION_LOCAL_ID = LdapAttrs.uniqueIdentifier.name(); // public String getId(); - public UUID getUuid(); + UUID getUuid(); - public LdapName getUserDn(); + LdapName getUserDn(); - public String getLocalId(); + String getLocalId(); - public Authorization getAuthorization(); + Authorization getAuthorization(); - public Session getDataSession(String cn, String workspace, Repository repository); + boolean isAnonymous(); - public void releaseDataSession(String cn, Session session); + ZonedDateTime getCreationTime(); + ZonedDateTime getEnd(); + + boolean isValid(); + + // public Session getDataSession(String cn, String workspace, Repository + // repository); + // + // public void releaseDataSession(String cn, Session session); // public void addHttpSession(HttpServletRequest request); diff --git a/org.argeo.cms/src/org/argeo/cms/auth/CurrentUser.java b/org.argeo.cms/src/org/argeo/cms/auth/CurrentUser.java index 375600ad2..2f6325d27 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/CurrentUser.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/CurrentUser.java @@ -21,7 +21,6 @@ import java.security.PrivilegedAction; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.security.acl.Group; -import java.util.Collection; import java.util.HashSet; import java.util.Set; import java.util.UUID; @@ -32,14 +31,9 @@ import javax.security.auth.x500.X500Principal; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.argeo.cms.CmsException; -import org.argeo.cms.internal.http.WebCmsSessionImpl; +import org.argeo.cms.internal.auth.CmsSessionImpl; import org.argeo.eclipse.ui.specific.UiContext; import org.argeo.node.NodeConstants; -import org.argeo.node.security.NodeAuthenticated; -import org.osgi.framework.BundleContext; -import org.osgi.framework.FrameworkUtil; -import org.osgi.framework.InvalidSyntaxException; -import org.osgi.framework.ServiceReference; import org.osgi.service.useradmin.Authorization; /** @@ -48,7 +42,7 @@ import org.osgi.service.useradmin.Authorization; */ public final class CurrentUser { private final static Log log = LogFactory.getLog(CurrentUser.class); - private final static BundleContext bc = FrameworkUtil.getBundle(CurrentUser.class).getBundleContext(); +// private final static BundleContext bc = FrameworkUtil.getBundle(CurrentUser.class).getBundleContext(); /* * CURRENT USER API */ @@ -135,9 +129,9 @@ public final class CurrentUser { */ private static Subject currentSubject() { - NodeAuthenticated cmsView = getNodeAuthenticated(); + CmsAuthenticated cmsView = getNodeAuthenticated(); if (cmsView != null) - return cmsView.getLoginContext().getSubject(); + return cmsView.getSubject(); Subject subject = Subject.getSubject(AccessController.getContext()); if (subject != null) return subject; @@ -149,8 +143,8 @@ public final class CurrentUser { * display, or null if none is available from this call. Not API: Only * for low-level access. */ - private static NodeAuthenticated getNodeAuthenticated() { - return UiContext.getData(NodeAuthenticated.KEY); + private static CmsAuthenticated getNodeAuthenticated() { + return UiContext.getData(CmsAuthenticated.KEY); } private static Authorization getAuthorization(Subject subject) { @@ -163,24 +157,30 @@ public final class CurrentUser { nodeSessionId = subject.getPrivateCredentials(CmsSessionId.class).iterator().next().getUuid(); else return false; - Collection> srs; - try { - srs = bc.getServiceReferences(CmsSession.class, "(" + CmsSession.SESSION_UUID + "=" + nodeSessionId + ")"); - } catch (InvalidSyntaxException e) { - throw new CmsException("Cannot retrieve CMS session #" + nodeSessionId, e); - } - - if (srs.size() == 0) { - // if (log.isTraceEnabled()) - // log.warn("No CMS web session found for http session " + - // nodeSessionId); - return false; - } else if (srs.size() > 1) - throw new CmsException(srs.size() + " CMS web sessions found for http session " + nodeSessionId); - - WebCmsSessionImpl cmsSession = (WebCmsSessionImpl) bc.getService(srs.iterator().next()); - cmsSession.cleanUp(); -// subject.getPrivateCredentials().removeAll(subject.getPrivateCredentials(CmsSessionId.class)); + CmsSessionImpl cmsSession = (CmsSessionImpl) CmsSessionImpl.getByUuid(nodeSessionId.toString()); + cmsSession.close(); + // Collection> srs; + // try { + // srs = bc.getServiceReferences(CmsSession.class, "(" + + // CmsSession.SESSION_UUID + "=" + nodeSessionId + ")"); + // } catch (InvalidSyntaxException e) { + // throw new CmsException("Cannot retrieve CMS session #" + + // nodeSessionId, e); + // } + // + // if (srs.size() == 0) { + // // if (log.isTraceEnabled()) + // // log.warn("No CMS web session found for http session " + + // // nodeSessionId); + // return false; + // } else if (srs.size() > 1) + // throw new CmsException(srs.size() + " CMS web sessions found for http + // session " + nodeSessionId); + // + // WebCmsSessionImpl cmsSession = (WebCmsSessionImpl) + // bc.getService(srs.iterator().next()); +// cmsSession.cleanUp(); + // subject.getPrivateCredentials().removeAll(subject.getPrivateCredentials(CmsSessionId.class)); if (log.isDebugEnabled()) log.debug("Logged out CMS session " + cmsSession.getUuid()); return true; diff --git a/org.argeo.cms/src/org/argeo/cms/auth/HttpSessionLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/HttpSessionLoginModule.java index ce004c58e..9cf32974f 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/HttpSessionLoginModule.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/HttpSessionLoginModule.java @@ -68,7 +68,7 @@ public class HttpSessionLoginModule implements LoginModule { return false; authorization = (Authorization) request.getAttribute(HttpContext.AUTHORIZATION); if (authorization == null) {// search by session ID - String httpSessionId = request.getSession().getId(); + String httpSessionId = request.getSession(false).getId(); // authorization = (Authorization) // request.getSession().getAttribute(HttpContext.AUTHORIZATION); // if (authorization == null) { diff --git a/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java index 237e07421..7e95c951a 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java @@ -187,8 +187,8 @@ public class UserAdminLoginModule implements LoginModule { @Override public boolean logout() throws LoginException { - if (log.isDebugEnabled()) - log.debug("Logging out from CMS... " + subject); + if (log.isTraceEnabled()) + log.trace("Logging out from CMS... " + subject); // boolean httpSessionLogoutOk = CmsAuthUtils.logoutSession(bc, // subject); CmsAuthUtils.cleanUp(subject); diff --git a/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsSessionImpl.java b/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsSessionImpl.java index 0b35d1d64..af29d2594 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsSessionImpl.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsSessionImpl.java @@ -4,6 +4,7 @@ import java.security.AccessControlContext; import java.security.AccessController; import java.security.PrivilegedAction; import java.security.PrivilegedExceptionAction; +import java.time.ZonedDateTime; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -18,12 +19,15 @@ import javax.jcr.Session; import javax.naming.InvalidNameException; import javax.naming.ldap.LdapName; import javax.security.auth.Subject; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.argeo.cms.CmsException; import org.argeo.cms.auth.CmsSession; import org.argeo.jcr.JcrUtils; +import org.argeo.node.NodeConstants; import org.argeo.node.security.NodeSecurityUtils; import org.osgi.framework.BundleContext; import org.osgi.framework.FrameworkUtil; @@ -44,6 +48,9 @@ public class CmsSessionImpl implements CmsSession { private final LdapName userDn; private final boolean anonymous; + private final ZonedDateTime creationTime; + private ZonedDateTime end; + private ServiceRegistration serviceRegistration; private Map dataSessions = new HashMap<>(); @@ -51,6 +58,7 @@ public class CmsSessionImpl implements CmsSession { private LinkedHashSet additionalDataSessions = new LinkedHashSet<>(); public CmsSessionImpl(Subject initialSubject, Authorization authorization, String localSessionId) { + this.creationTime = ZonedDateTime.now(); this.initialContext = Subject.doAs(initialSubject, new PrivilegedAction() { @Override @@ -82,7 +90,8 @@ public class CmsSessionImpl implements CmsSession { serviceRegistration = bc.registerService(CmsSession.class, this, props); } - public synchronized void cleanUp() { + public synchronized void close() { + end = ZonedDateTime.now(); serviceRegistration.unregister(); // TODO check data session in use ? @@ -90,10 +99,25 @@ public class CmsSessionImpl implements CmsSession { JcrUtils.logoutQuietly(dataSessions.get(path)); for (Session session : additionalDataSessions) JcrUtils.logoutQuietly(session); + + try { + LoginContext lc; + if (isAnonymous()) { + lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_ANONYMOUS, getSubject()); + } else { + lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, getSubject()); + } + lc.logout(); + } catch (LoginException e) { + log.warn("Could not logout " + getSubject() + ": " + e); + } notifyAll(); } - @Override + private Subject getSubject() { + return Subject.getSubject(initialContext); + } + public synchronized Session getDataSession(String cn, String workspace, Repository repository) { // FIXME make it more robust if (workspace == null) @@ -129,8 +153,7 @@ public class CmsSessionImpl implements CmsSession { private Session login(Repository repository, String workspace) { try { - Subject initialSubject = Subject.getSubject(initialContext); - return Subject.doAs(initialSubject, new PrivilegedExceptionAction() { + return Subject.doAs(getSubject(), new PrivilegedExceptionAction() { @Override public Session run() throws Exception { return repository.login(workspace); @@ -141,7 +164,6 @@ public class CmsSessionImpl implements CmsSession { } } - @Override public synchronized void releaseDataSession(String cn, Session session) { if (additionalDataSessions.contains(session)) { JcrUtils.logoutQuietly(session); @@ -158,6 +180,15 @@ public class CmsSessionImpl implements CmsSession { notifyAll(); } + @Override + public boolean isValid() { + return !isClosed(); + } + + protected boolean isClosed() { + return getEnd() != null; + } + @Override public Authorization getAuthorization() { return authorization; @@ -190,6 +221,16 @@ public class CmsSessionImpl implements CmsSession { return anonymous; } + @Override + public ZonedDateTime getCreationTime() { + return creationTime; + } + + @Override + public ZonedDateTime getEnd() { + return end; + } + public String toString() { return "CMS Session " + userDn + " local=" + localSessionId + ", uuid=" + uuid; } @@ -229,4 +270,21 @@ public class CmsSessionImpl implements CmsSession { throw new CmsException(sr.size() + " CMS sessions registered for " + uuid); } + + public static void closeInvalidSessions() { + Collection> srs; + try { + srs = bc.getServiceReferences(CmsSession.class, null); + for (ServiceReference sr : srs) { + CmsSession cmsSession = bc.getService(sr); + if (!cmsSession.isValid()) { + ((CmsSessionImpl) cmsSession).close(); + if (log.isDebugEnabled()) + log.debug("Closed expired CMS session " + cmsSession); + } + } + } catch (InvalidSyntaxException e) { + throw new CmsException("Cannot get CMS sessions", e); + } + } } diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/CmsSessionProvider.java b/org.argeo.cms/src/org/argeo/cms/internal/http/CmsSessionProvider.java index 37ba5cdb1..85390e62e 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/http/CmsSessionProvider.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/http/CmsSessionProvider.java @@ -12,7 +12,7 @@ import javax.servlet.http.HttpServletRequest; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.jackrabbit.server.SessionProvider; -import org.argeo.cms.auth.CmsSession; +import org.argeo.cms.internal.auth.CmsSessionImpl; import org.argeo.jcr.JcrUtils; /** @@ -26,7 +26,7 @@ class CmsSessionProvider implements SessionProvider, Serializable { private final String alias; - private LinkedHashMap cmsSessions = new LinkedHashMap<>(); + private LinkedHashMap cmsSessions = new LinkedHashMap<>(); public CmsSessionProvider(String alias) { this.alias = alias; @@ -35,9 +35,9 @@ class CmsSessionProvider implements SessionProvider, Serializable { public Session getSession(HttpServletRequest request, Repository rep, String workspace) throws javax.jcr.LoginException, ServletException, RepositoryException { - CmsSession cmsSession = WebCmsSessionImpl.getCmsSession(request); -// if (cmsSession == null) -// return anonymousSession(request, rep, workspace); + CmsSessionImpl cmsSession = (CmsSessionImpl) WebCmsSessionImpl.getCmsSession(request); + // if (cmsSession == null) + // return anonymousSession(request, rep, workspace); if (log.isTraceEnabled()) { log.debug("Get JCR session from " + cmsSession); } @@ -46,32 +46,35 @@ class CmsSessionProvider implements SessionProvider, Serializable { return session; } -// private synchronized Session anonymousSession(HttpServletRequest request, Repository repository, String workspace) { -// // TODO rather log in here as anonymous? -// LoginContext lc = (LoginContext) request.getAttribute(NodeConstants.LOGIN_CONTEXT_ANONYMOUS); -// if (lc == null) -// throw new CmsException("No login context available"); -// // optimize -// Session session; -// try { -// session = Subject.doAs(lc.getSubject(), new PrivilegedExceptionAction() { -// @Override -// public Session run() throws Exception { -// return repository.login(workspace); -// } -// }); -// } catch (Exception e) { -// throw new CmsException("Cannot log in to JCR", e); -// } -// return session; -// } + // private synchronized Session anonymousSession(HttpServletRequest request, + // Repository repository, String workspace) { + // // TODO rather log in here as anonymous? + // LoginContext lc = (LoginContext) + // request.getAttribute(NodeConstants.LOGIN_CONTEXT_ANONYMOUS); + // if (lc == null) + // throw new CmsException("No login context available"); + // // optimize + // Session session; + // try { + // session = Subject.doAs(lc.getSubject(), new + // PrivilegedExceptionAction() { + // @Override + // public Session run() throws Exception { + // return repository.login(workspace); + // } + // }); + // } catch (Exception e) { + // throw new CmsException("Cannot log in to JCR", e); + // } + // return session; + // } public synchronized void releaseSession(Session session) { if (cmsSessions.containsKey(session)) { - CmsSession cmsSession = cmsSessions.get(session); + CmsSessionImpl cmsSession = cmsSessions.get(session); cmsSession.releaseDataSession(alias, session); } else { - log.warn("JCR session "+session+" not found in CMS session list. Logging it out..."); + log.warn("JCR session " + session + " not found in CMS session list. Logging it out..."); JcrUtils.logoutQuietly(session); } } diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/HttpFilter.java b/org.argeo.cms/src/org/argeo/cms/internal/http/HttpFilter.java deleted file mode 100644 index 142bccb9d..000000000 --- a/org.argeo.cms/src/org/argeo/cms/internal/http/HttpFilter.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.argeo.cms.internal.http; - -import java.io.IOException; - -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; - -/** Abstract base class for http filters. */ -abstract class HttpFilter implements Filter { - // private final static Log log = LogFactory.getLog(HttpFilter.class); - - protected abstract void doFilter(HttpSession httpSession, - HttpServletRequest request, HttpServletResponse response, - FilterChain filterChain) throws IOException, ServletException; - - @Override - public void doFilter(ServletRequest servletRequest, - ServletResponse servletResponse, FilterChain filterChain) - throws IOException, ServletException { - HttpServletRequest request = (HttpServletRequest) servletRequest; - doFilter(request.getSession(), request, - (HttpServletResponse) servletResponse, filterChain); - } - - @Override - public void destroy() { - } - - @Override - public void init(FilterConfig arg0) throws ServletException { - } - -} diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/WebCmsSessionImpl.java b/org.argeo.cms/src/org/argeo/cms/internal/http/WebCmsSessionImpl.java index 06ff57355..3d5e3fe4c 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/http/WebCmsSessionImpl.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/http/WebCmsSessionImpl.java @@ -2,21 +2,36 @@ package org.argeo.cms.internal.http; import javax.security.auth.Subject; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; import org.argeo.cms.auth.CmsSession; import org.argeo.cms.internal.auth.CmsSessionImpl; import org.osgi.service.useradmin.Authorization; public class WebCmsSessionImpl extends CmsSessionImpl { + // private final static Log log = + // LogFactory.getLog(WebCmsSessionImpl.class); - public WebCmsSessionImpl(Subject initialSubject, Authorization authorization, String httpSessionId) { - super(initialSubject, authorization, httpSessionId); + private HttpSession httpSession; + + public WebCmsSessionImpl(Subject initialSubject, Authorization authorization, HttpServletRequest request) { + super(initialSubject, authorization, request.getSession(false).getId()); + httpSession = request.getSession(false); + } + + @Override + public boolean isValid() { + if (isClosed()) + return false; + try {// test http session + httpSession.getCreationTime(); + return true; + } catch (IllegalStateException ise) { + return false; + } } public static CmsSession getCmsSession(HttpServletRequest request) { -// CmsSession cmsSession = (CmsSession) request.getAttribute(CmsSession.class.getName()); -// if (cmsSession != null) -// return cmsSession; - return CmsSessionImpl.getByLocalId(request.getSession().getId()); + return CmsSessionImpl.getByLocalId(request.getSession(false).getId()); } } diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/KernelThread.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/KernelThread.java index 944a7cd3d..5ebf4055f 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/kernel/KernelThread.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/kernel/KernelThread.java @@ -8,6 +8,7 @@ import org.apache.commons.logging.LogFactory; import org.apache.jackrabbit.api.stats.RepositoryStatistics; import org.apache.jackrabbit.stats.RepositoryStatisticsImpl; import org.argeo.cms.CmsException; +import org.argeo.cms.internal.auth.CmsSessionImpl; /** * Background thread started by the {@link Kernel}, which gather statistics and @@ -35,6 +36,9 @@ class KernelThread extends Thread { } private void doSmallestPeriod() { + // Clean expired sessions + CmsSessionImpl.closeInvalidSessions(); + if (kernelStatsLog.isDebugEnabled()) { StringBuilder line = new StringBuilder(64); line.append("§\t"); diff --git a/org.argeo.jcr/src/org/argeo/jcr/proxy/ResourceProxyServlet.java b/org.argeo.jcr/src/org/argeo/jcr/proxy/ResourceProxyServlet.java index e92e2a402..0a90e64a5 100644 --- a/org.argeo.jcr/src/org/argeo/jcr/proxy/ResourceProxyServlet.java +++ b/org.argeo.jcr/src/org/argeo/jcr/proxy/ResourceProxyServlet.java @@ -54,7 +54,7 @@ public class ResourceProxyServlet extends HttpServlet { if (log.isTraceEnabled()) { log.trace("path=" + path); log.trace("UserPrincipal = " + request.getUserPrincipal().getName()); - log.trace("SessionID = " + request.getSession().getId()); + log.trace("SessionID = " + request.getSession(false).getId()); log.trace("ContextPath = " + request.getContextPath()); log.trace("ServletPath = " + request.getServletPath()); log.trace("PathInfo = " + request.getPathInfo()); diff --git a/org.argeo.node.api/src/org/argeo/node/security/NodeAuthenticated.java b/org.argeo.node.api/src/org/argeo/node/security/NodeAuthenticated.java deleted file mode 100644 index a316af55a..000000000 --- a/org.argeo.node.api/src/org/argeo/node/security/NodeAuthenticated.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.argeo.node.security; - -import javax.security.auth.login.LoginContext; - -public interface NodeAuthenticated { - String KEY = "org.argeo.node.authenticated"; - -// Subject getSubject(); - LoginContext getLoginContext(); - -} diff --git a/org.argeo.util/src/org/argeo/util/LangUtils.java b/org.argeo.util/src/org/argeo/util/LangUtils.java index fae631234..b791f490d 100644 --- a/org.argeo.util/src/org/argeo/util/LangUtils.java +++ b/org.argeo.util/src/org/argeo/util/LangUtils.java @@ -7,6 +7,9 @@ import java.io.Writer; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; import java.util.Dictionary; import java.util.Enumeration; import java.util.Hashtable; @@ -157,6 +160,30 @@ public class LangUtils { chainCauseMessage(buf, t.getCause()); } + /* + * TIME + */ + /** Formats time elapsed since start. */ + public static String since(ZonedDateTime start) { + ZonedDateTime now = ZonedDateTime.now(); + return duration(start, now); + } + + /** Formats a duration. */ + public static String duration(Temporal start, Temporal end) { + long count = ChronoUnit.DAYS.between(start, end); + if (count != 0) + return count > 1 ? count + " days" : count + " day"; + count = ChronoUnit.HOURS.between(start, end); + if (count != 0) + return count > 1 ? count + " hours" : count + " hours"; + count = ChronoUnit.MINUTES.between(start, end); + if (count != 0) + return count > 1 ? count + " minutes" : count + " minute"; + count = ChronoUnit.SECONDS.between(start, end); + return count > 1 ? count + " seconds" : count + " second"; + } + /** Singleton constructor. */ private LangUtils() { -- 2.30.2