diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/beans/UserAccount.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/beans/UserAccount.java index 217648473..2838344f3 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/beans/UserAccount.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/beans/UserAccount.java @@ -54,6 +54,7 @@ public class UserAccount { private boolean passwordChangeRequired = false; private int loginCount = 0; // Never negative. + private long lastLoginTime = 0L; // Never negative. private Status status = Status.INACTIVE; // Might be null. private String externalAuthId = ""; // Never null. @@ -143,6 +144,14 @@ public class UserAccount { this.loginCount = Math.max(0, loginCount); } + public long getLastLoginTime() { + return lastLoginTime; + } + + public void setLastLoginTime(long lastLoginTime) { + this.lastLoginTime = Math.max(0, lastLoginTime); + } + public Status getStatus() { return status; } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/accounts/UserAccountsOrdering.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/accounts/UserAccountsOrdering.java index a21f61743..a0ab54e4e 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/accounts/UserAccountsOrdering.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/accounts/UserAccountsOrdering.java @@ -34,7 +34,8 @@ public class UserAccountsOrdering { public enum Field { EMAIL("email"), FIRST_NAME("firstName"), LAST_NAME("lastName"), STATUS( - "status"), ROLE("ps"), LOGIN_COUNT("count"); + "status"), ROLE("ps"), LOGIN_COUNT("count"), LAST_LOGIN_TIME( + "lastLogin"); public static Field DEFAULT_FIELD = EMAIL; diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/accounts/UserAccountsSelector.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/accounts/UserAccountsSelector.java index 315f8408a..b597479d2 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/accounts/UserAccountsSelector.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/accounts/UserAccountsSelector.java @@ -39,7 +39,8 @@ public class UserAccountsSelector { + "PREFIX fn: \n" + "PREFIX auth: \n"; - private static final String ALL_VARIABLES = "?uri ?email ?firstName ?lastName ?pwd ?expire ?count ?status ?isRoot"; + private static final String ALL_VARIABLES = "?uri ?email ?firstName " + + "?lastName ?pwd ?expire ?count ?lastLogin ?status ?isRoot"; private static final String COUNT_VARIABLE = "?uri"; @@ -169,6 +170,7 @@ public class UserAccountsSelector { + " OPTIONAL { ?uri auth:md5password ?pwd } \n" + " OPTIONAL { ?uri auth:passwordChangeExpires ?expire } \n" + " OPTIONAL { ?uri auth:loginCount ?count } \n" + + " OPTIONAL { ?uri auth:lastLoginTime ?lastLogin } \n" + " OPTIONAL { ?uri auth:status ?status } \n" + " OPTIONAL { ?uri ?isRoot auth:RootUserAccount }"; } @@ -326,6 +328,7 @@ public class UserAccountsSelector { user.setMd5Password(ifLiteralPresent(solution, "pwd", "")); user.setPasswordLinkExpires(ifLongPresent(solution, "expire", 0L)); user.setLoginCount(ifIntPresent(solution, "count", 0)); + user.setLastLoginTime(ifLongPresent(solution, "lastLogin", 0)); user.setStatus(parseStatus(solution, "status", null)); user.setRootUser(solution.contains("isRoot")); return user; diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/accounts/admin/UserAccountsAddPage.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/accounts/admin/UserAccountsAddPage.java index 54221cda3..a5a9ca196 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/accounts/admin/UserAccountsAddPage.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/accounts/admin/UserAccountsAddPage.java @@ -141,6 +141,7 @@ public class UserAccountsAddPage extends UserAccountsPage { u.setPasswordChangeRequired(false); u.setPasswordLinkExpires(0); u.setLoginCount(0); + u.setLastLoginTime(0L); u.setStatus(Status.INACTIVE); u.setPermissionSetUris(Collections.singleton(selectedRoleUri)); diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/accounts/admin/UserAccountsListPage.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/accounts/admin/UserAccountsListPage.java index 3da82e412..c3ed4fa18 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/accounts/admin/UserAccountsListPage.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/accounts/admin/UserAccountsListPage.java @@ -7,6 +7,7 @@ import static edu.cornell.mannlib.vitro.webapp.controller.accounts.UserAccountsS import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -34,9 +35,6 @@ import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.Tem /** * Handle the List page. * - * TODO: agree with Manolo how to do the column heads as links that change the - * sort order - * * TODO: auto-complete */ public class UserAccountsListPage extends UserAccountsPage { @@ -242,6 +240,15 @@ public class UserAccountsListPage extends UserAccountsPage { return account.getLoginCount(); } + public Date getLastLoginTime() { + long time = account.getLastLoginTime(); + if (time > 0L) { + return new Date(time); + } else { + return null; + } + } + public String getStatus() { Status status = account.getStatus(); if (status == null) { @@ -299,7 +306,7 @@ public class UserAccountsListPage extends UserAccountsPage { HttpSession session = req.getSession(); Object o = session.getAttribute(ATTRIBUTE); session.removeAttribute(ATTRIBUTE); - + if (o instanceof Message) { ((Message) o).applyToBodyMap(body); } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/authenticate/BasicAuthenticator.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/authenticate/BasicAuthenticator.java index 6ebb7a4fd..7b43b71c1 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/authenticate/BasicAuthenticator.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/authenticate/BasicAuthenticator.java @@ -3,6 +3,7 @@ package edu.cornell.mannlib.vitro.webapp.controller.authenticate; import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.Map; @@ -137,6 +138,7 @@ public class BasicAuthenticator extends Authenticator { */ private void recordLoginOnUserRecord(UserAccount userAccount) { userAccount.setLoginCount(userAccount.getLoginCount() + 1); + userAccount.setLastLoginTime(new Date().getTime()); getUserAccountsDao().updateUserAccount(userAccount); } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/VitroVocabulary.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/VitroVocabulary.java index f8efc288b..36c80287a 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/VitroVocabulary.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/VitroVocabulary.java @@ -157,6 +157,7 @@ public class VitroVocabulary { public static final String USERACCOUNT_MD5_PASSWORD = VITRO_AUTH + "md5password"; public static final String USERACCOUNT_OLD_PASSWORD = VITRO_AUTH + "oldpassword"; public static final String USERACCOUNT_LOGIN_COUNT = VITRO_AUTH + "loginCount"; + public static final String USERACCOUNT_LAST_LOGIN_TIME = VITRO_AUTH + "lastLoginTime"; public static final String USERACCOUNT_STATUS = VITRO_AUTH + "status"; public static final String USERACCOUNT_PASSWORD_LINK_EXPIRES = VITRO_AUTH + "passwordLinkExpires"; public static final String USERACCOUNT_PASSWORD_CHANGE_REQUIRED = VITRO_AUTH + "passwordChangeRequired"; diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaBaseDaoCon.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaBaseDaoCon.java index f69d8b2c5..9b8e2545c 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaBaseDaoCon.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/JenaBaseDaoCon.java @@ -127,6 +127,7 @@ public class JenaBaseDaoCon { protected DatatypeProperty USERACCOUNT_MD5_PASSWORD = _constModel.createDatatypeProperty(VitroVocabulary.USERACCOUNT_MD5_PASSWORD); protected DatatypeProperty USERACCOUNT_OLD_PASSWORD = _constModel.createDatatypeProperty(VitroVocabulary.USERACCOUNT_OLD_PASSWORD); protected DatatypeProperty USERACCOUNT_LOGIN_COUNT = _constModel.createDatatypeProperty(VitroVocabulary.USERACCOUNT_LOGIN_COUNT); + protected DatatypeProperty USERACCOUNT_LAST_LOGIN_TIME = _constModel.createDatatypeProperty(VitroVocabulary.USERACCOUNT_LAST_LOGIN_TIME); protected DatatypeProperty USERACCOUNT_STATUS = _constModel.createDatatypeProperty(VitroVocabulary.USERACCOUNT_STATUS); protected DatatypeProperty USERACCOUNT_PASSWORD_LINK_EXPIRES = _constModel.createDatatypeProperty(VitroVocabulary.USERACCOUNT_PASSWORD_LINK_EXPIRES); protected DatatypeProperty USERACCOUNT_PASSWORD_CHANGE_REQUIRED = _constModel.createDatatypeProperty(VitroVocabulary.USERACCOUNT_PASSWORD_CHANGE_REQUIRED); diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/UserAccountsDaoJena.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/UserAccountsDaoJena.java index 3621cbf34..64ca0bd5c 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/UserAccountsDaoJena.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/dao/jena/UserAccountsDaoJena.java @@ -98,6 +98,7 @@ public class UserAccountsDaoJena extends JenaBaseDao implements UserAccountsDao u.setPasswordChangeRequired(getPropertyBooleanValue(r, USERACCOUNT_PASSWORD_CHANGE_REQUIRED)); u.setLoginCount(getPropertyIntValue(r, USERACCOUNT_LOGIN_COUNT)); + u.setLastLoginTime(getPropertyLongValue(r, USERACCOUNT_LAST_LOGIN_TIME)); u.setStatusFromString(getPropertyStringValue(r, USERACCOUNT_STATUS)); u.setExternalAuthId(getPropertyStringValue(r, USERACCOUNT_EXTERNAL_AUTH_ID)); @@ -190,6 +191,8 @@ public class UserAccountsDaoJena extends JenaBaseDao implements UserAccountsDao userAccount.isPasswordChangeRequired(), model); addPropertyIntValue(res, USERACCOUNT_LOGIN_COUNT, userAccount.getLoginCount(), model); + addPropertyLongValue(res, USERACCOUNT_LAST_LOGIN_TIME, + userAccount.getLastLoginTime(), model); if (userAccount.getStatus() != null) { addPropertyStringValue(res, USERACCOUNT_STATUS, userAccount .getStatus().toString(), model); @@ -247,6 +250,8 @@ public class UserAccountsDaoJena extends JenaBaseDao implements UserAccountsDao userAccount.isPasswordChangeRequired(), model, true); updatePropertyIntValue(res, USERACCOUNT_LOGIN_COUNT, userAccount.getLoginCount(), model); + updatePropertyLongValue(res, USERACCOUNT_LAST_LOGIN_TIME, + userAccount.getLastLoginTime(), model); if (userAccount.getStatus() == null) { updatePropertyStringValue(res, USERACCOUNT_STATUS, null, model); } else { diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/controller/accounts/UserAccountsSelectorTest.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/controller/accounts/UserAccountsSelectorTest.java index 4a08a9766..5194dfd7c 100644 --- a/webapp/test/edu/cornell/mannlib/vitro/webapp/controller/accounts/UserAccountsSelectorTest.java +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/controller/accounts/UserAccountsSelectorTest.java @@ -84,6 +84,7 @@ public class UserAccountsSelectorTest extends AbstractTestClass { assertEquals("password", "garbage", acct.getMd5Password()); assertEquals("expires", 1100234965897L, acct.getPasswordLinkExpires()); assertEquals("loginCount", 50, acct.getLoginCount()); + assertEquals("lastLogin", 1020304050607080L, acct.getLastLoginTime()); assertEquals("status", UserAccount.Status.ACTIVE, acct.getStatus()); assertEqualSets( "permissions", @@ -107,6 +108,7 @@ public class UserAccountsSelectorTest extends AbstractTestClass { assertEquals("password", "garbage", acct.getMd5Password()); assertEquals("expires", 0L, acct.getPasswordLinkExpires()); assertEquals("loginCount", 7, acct.getLoginCount()); + assertEquals("lastLogin", 1122334455667788L, acct.getLastLoginTime()); assertEquals("status", UserAccount.Status.ACTIVE, acct.getStatus()); assertEqualSets("permissions", Collections. emptySet(), acct.getPermissionSetUris()); @@ -237,6 +239,23 @@ public class UserAccountsSelectorTest extends AbstractTestClass { assertSelectedUris(10, "user10", "user04", "user08"); } + @Test + public void sortByLastLoginTimeAscending() { + UserAccountsOrdering orderBy = new UserAccountsOrdering( + Field.LAST_LOGIN_TIME, Direction.ASCENDING); + selectOnCriteria(3, 1, orderBy, "", ""); + // user06 has no login count: reads as 0. + assertSelectedUris(10, "user07", "user03", "user06"); + } + + @Test + public void sortByLastLoginTimeDescending() { + UserAccountsOrdering orderBy = new UserAccountsOrdering( + Field.LAST_LOGIN_TIME, Direction.DESCENDING); + selectOnCriteria(3, 1, orderBy, "", ""); + assertSelectedUris(10, "user08", "user10", "user09"); + } + // ---------------------------------------------------------------------- // filtering tests // ---------------------------------------------------------------------- diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/controller/accounts/UserAccountsSelectorTest.n3 b/webapp/test/edu/cornell/mannlib/vitro/webapp/controller/accounts/UserAccountsSelectorTest.n3 index 7e39d30fb..c6c8fe097 100644 --- a/webapp/test/edu/cornell/mannlib/vitro/webapp/controller/accounts/UserAccountsSelectorTest.n3 +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/controller/accounts/UserAccountsSelectorTest.n3 @@ -23,6 +23,7 @@ mydomain:user01 auth:md5password "garbage" ; auth:passwordChangeExpires 0 ; auth:loginCount 5 ; + auth:lastLoginTime 100 ; auth:status "ACTIVE" ; auth:hasPermissionSet mydomain:role1 ; . @@ -35,6 +36,7 @@ mydomain:user02 auth:md5password "garbage" ; auth:passwordChangeExpires 0 ; auth:loginCount 5 ; + auth:lastLoginTime 100 ; auth:status "INACTIVE" ; auth:hasPermissionSet mydomain:role1 ; . @@ -47,6 +49,7 @@ mydomain:user03 auth:md5password "garbage" ; auth:passwordChangeExpires 0 ; auth:loginCount 0 ; + auth:lastLoginTime 1 ; auth:status "INACTIVE" ; auth:hasPermissionSet mydomain:role1 ; auth:hasPermissionSet mydomain:role2 ; @@ -60,6 +63,7 @@ mydomain:user04 # auth:md5password NONE ; auth:passwordChangeExpires 0 ; auth:loginCount 9 ; + auth:lastLoginTime 3 ; auth:status "ACTIVE" ; . @@ -71,6 +75,7 @@ mydomain:user05 auth:md5password "garbage" ; # auth:passwordChangeExpires NONE ; auth:loginCount 2 ; + auth:lastLoginTime 100 ; auth:status "ACTIVE" ; auth:hasPermissionSet mydomain:role1 ; . @@ -83,6 +88,7 @@ mydomain:user06 auth:md5password "garbage" ; auth:passwordChangeExpires 0 ; # auth:loginCount NONE ; + auth:lastLoginTime 2 ; auth:status "INACTIVE" ; auth:hasPermissionSet mydomain:role1 ; . @@ -95,6 +101,7 @@ mydomain:user07 auth:md5password "garbage" ; auth:passwordChangeExpires 0 ; auth:loginCount 1 ; +# auth:lastLoginTime NONE ; # auth:status NONE ; auth:hasPermissionSet mydomain:role2 ; . @@ -108,6 +115,7 @@ mydomain:user08 auth:md5password "garbage" ; auth:passwordChangeExpires 0 ; auth:loginCount 7 ; + auth:lastLoginTime 1122334455667788 ; auth:status "ACTIVE" ; . @@ -119,6 +127,7 @@ mydomain:user09 auth:md5password "garbage" ; auth:passwordChangeExpires 0 ; auth:loginCount 3 ; + auth:lastLoginTime 1000000000000000 ; auth:status "ACTIVE" ; auth:hasPermissionSet mydomain:role1 ; . @@ -129,8 +138,9 @@ mydomain:user10 auth:firstName "Bob" ; auth:lastName "Caruso" ; auth:md5password "garbage" ; - auth:passwordChangeExpires "1100234965897" ; + auth:passwordChangeExpires 1100234965897 ; auth:loginCount 50 ; + auth:lastLoginTime 1020304050607080 ; auth:status "ACTIVE" ; auth:hasPermissionSet mydomain:role2 ; . diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/dao/jena/UserAccountsDaoJenaTest.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/dao/jena/UserAccountsDaoJenaTest.java index 7f98d9c84..f48ee72c4 100644 --- a/webapp/test/edu/cornell/mannlib/vitro/webapp/dao/jena/UserAccountsDaoJenaTest.java +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/dao/jena/UserAccountsDaoJenaTest.java @@ -2,7 +2,6 @@ package edu.cornell.mannlib.vitro.webapp.dao.jena; -import static org.junit.Assert.*; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; @@ -86,6 +85,7 @@ public class UserAccountsDaoJenaTest extends AbstractTestClass { assertEquals("linkExpires", 0L, u.getPasswordLinkExpires()); assertEquals("changeRequired", false, u.isPasswordChangeRequired()); assertEquals("loginCount", 5, u.getLoginCount()); + assertEquals("loginTime", 12345678L, u.getLastLoginTime()); assertEquals("status", Status.ACTIVE, u.getStatus()); assertEquals("externalAuthId", "user1", u.getExternalAuthId()); assertEquals("permissionSetUris", Collections.singleton(URI_ROLE1), @@ -141,6 +141,7 @@ public class UserAccountsDaoJenaTest extends AbstractTestClass { in.setPasswordLinkExpires(999966663333L); in.setPasswordChangeRequired(true); in.setLoginCount(42); + in.setLastLoginTime(8877665544332211L); in.setStatus(Status.INACTIVE); in.setExternalAuthId("newUser"); in.setPermissionSetUris(buildSet(URI_ROLE1, URI_ROLE2)); @@ -157,6 +158,7 @@ public class UserAccountsDaoJenaTest extends AbstractTestClass { assertEquals("linkExpires", 999966663333L, u.getPasswordLinkExpires()); assertEquals("changeRequired", true, u.isPasswordChangeRequired()); assertEquals("loginCount", 42, u.getLoginCount()); + assertEquals("lastLoginTime", 8877665544332211L, u.getLastLoginTime()); assertEquals("status", Status.INACTIVE, u.getStatus()); assertEquals("externalAuthId", "newUser", u.getExternalAuthId()); assertEquals("permissionSetUris", buildSet(URI_ROLE1, URI_ROLE2), @@ -188,6 +190,7 @@ public class UserAccountsDaoJenaTest extends AbstractTestClass { up.setPasswordLinkExpires(1L); up.setPasswordChangeRequired(false); up.setLoginCount(43); + up.setLastLoginTime(1020304050607080L); up.setStatus(Status.ACTIVE); up.setExternalAuthId("updatedUser1"); up.setPermissionSetUris(buildSet(URI_ROLE1, URI_ROLE3)); @@ -204,6 +207,7 @@ public class UserAccountsDaoJenaTest extends AbstractTestClass { assertEquals("changeExpires", 1L, u.getPasswordLinkExpires()); assertEquals("changeRequired", false, u.isPasswordChangeRequired()); assertEquals("loginCount", 43, u.getLoginCount()); + assertEquals("lastLoginTime", 1020304050607080L, u.getLastLoginTime()); assertEquals("status", Status.ACTIVE, u.getStatus()); assertEquals("externalAuthId", "updatedUser1", u.getExternalAuthId()); assertEquals("permissionSetUris", buildSet(URI_ROLE1, URI_ROLE3), diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/dao/jena/resources/UserAccountsDaoJenaTest.n3 b/webapp/test/edu/cornell/mannlib/vitro/webapp/dao/jena/resources/UserAccountsDaoJenaTest.n3 index 56d26e55c..7b5069c26 100644 --- a/webapp/test/edu/cornell/mannlib/vitro/webapp/dao/jena/resources/UserAccountsDaoJenaTest.n3 +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/dao/jena/resources/UserAccountsDaoJenaTest.n3 @@ -16,6 +16,7 @@ mydomain:user01 auth:md5password "garbage" ; auth:passwordChangeExpires 0 ; auth:loginCount 5 ; + auth:lastLoginTime 12345678 ; auth:status "ACTIVE" ; auth:externalAuthId "user1"; auth:hasPermissionSet mydomain:role1 ; diff --git a/webapp/web/templates/freemarker/body/accounts/userAccounts-list.ftl b/webapp/web/templates/freemarker/body/accounts/userAccounts-list.ftl index 66d72483e..e616f9cd7 100644 --- a/webapp/web/templates/freemarker/body/accounts/userAccounts-list.ftl +++ b/webapp/web/templates/freemarker/body/accounts/userAccounts-list.ftl @@ -122,6 +122,14 @@ + + + Last Login Time + + @@ -144,6 +152,13 @@ ${account.loginCount} + + <#if account.lastLoginTime??> + ${account.lastLoginTime?datetime?string.full} + <#else> +   + +