diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/accounts/UserAccountsSelectionCriteria.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/accounts/UserAccountsSelectionCriteria.java index 65f1d6a3b..42f0b854f 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/accounts/UserAccountsSelectionCriteria.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/accounts/UserAccountsSelectionCriteria.java @@ -2,9 +2,11 @@ package edu.cornell.mannlib.vitro.webapp.controller.accounts; - /** * On what basis are we selecting user accounts? + * + * Search terms are matched against email, and against firstName combined with + * lastName. Searches are case-insensitive. */ public class UserAccountsSelectionCriteria { public static final int DEFAULT_ACCOUNTS_PER_PAGE = 25; 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 8ebd07685..9c4c51cb1 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 @@ -36,6 +36,7 @@ public class UserAccountsSelector { private static final String PREFIX_LINES = "" + "PREFIX rdf: \n" + + "PREFIX fn: \n" + "PREFIX auth: \n"; private static final String ALL_VARIABLES = "?uri ?email ?firstName ?lastName ?pwd ?expire ?count ?status"; @@ -59,6 +60,7 @@ public class UserAccountsSelector { + "SELECT count(DISTINCT %countVariable%) \n" // + "WHERE {\n" // + " %requiredClauses% \n" // + + " %optionalClauses% \n" // + " %filterClauses% \n" // + "} \n"; @@ -71,6 +73,13 @@ public class UserAccountsSelector { private static final Syntax SYNTAX = Syntax.syntaxARQ; + /** + * If the user enters any of these characters in a search term, escape it + * with a backslash. + */ + private static final char[] REGEX_SPECIAL_CHARACTERS = "[\\^$.|?*+()]" + .toCharArray(); + /** * Convenience method. */ @@ -124,6 +133,7 @@ public class UserAccountsSelector { .replace("%prefixes%", PREFIX_LINES) .replace("%countVariable%", COUNT_VARIABLE) .replace("%requiredClauses%", requiredClauses()) + .replace("%optionalClauses%", optionalClauses()) .replace("%filterClauses%", filterClauses()); log.debug("count query: " + qString); @@ -163,8 +173,52 @@ public class UserAccountsSelector { } private String filterClauses() { - log.warn("UserAccountsSelector.filterClauses() not implemented."); - return ""; + String filters = ""; + + String roleFilterUri = criteria.getRoleFilterUri(); + String searchTerm = criteria.getSearchTerm(); + + if (!roleFilterUri.isEmpty()) { + String clean = escapeForRegex(roleFilterUri); + filters += "OPTIONAL { ?uri auth:hasPermissionSet ?role } \n" + + " FILTER (REGEX(str(?role), '" + clean + "'))"; + } + + if ((!roleFilterUri.isEmpty()) && (!searchTerm.isEmpty())) { + filters += " \n "; + } + + if (!searchTerm.isEmpty()) { + String clean = escapeForRegex(searchTerm); + filters += "FILTER (" + + ("REGEX(?email, '" + clean + "', 'i')") + + " || " + + ("REGEX(fn:concat(?firstName, ' ', ?lastName), '" + clean + "', 'i')") + + ")"; + } + + return filters; + } + + /** + * Escape any regex special characters in the string. + * + * Note that the SPARQL + * parser requires two backslashes, in order to pass a single backslash to + * the REGEX function. + */ + private String escapeForRegex(String raw) { + StringBuilder clean = new StringBuilder(); + outer: for (char c : raw.toCharArray()) { + for (char special : REGEX_SPECIAL_CHARACTERS) { + if (c == special) { + clean.append('\\').append('\\').append(c); + continue outer; + } + } + clean.append(c); + } + return clean.toString(); } /** Sort as desired, and within ties, sort by EMail address. */ 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 773f3d888..11d344a3d 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 @@ -3,9 +3,7 @@ package edu.cornell.mannlib.vitro.webapp.controller.accounts; import static edu.cornell.mannlib.vitro.webapp.controller.accounts.UserAccountsOrdering.DEFAULT_ORDERING; -import static org.junit.Assert.*; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; import java.io.IOException; import java.io.InputStream; @@ -26,10 +24,6 @@ import com.hp.hpl.jena.rdf.model.ModelFactory; import edu.cornell.mannlib.vitro.testing.AbstractTestClass; import edu.cornell.mannlib.vitro.webapp.beans.UserAccount; -import edu.cornell.mannlib.vitro.webapp.controller.accounts.UserAccountsOrdering; -import edu.cornell.mannlib.vitro.webapp.controller.accounts.UserAccountsSelection; -import edu.cornell.mannlib.vitro.webapp.controller.accounts.UserAccountsSelectionCriteria; -import edu.cornell.mannlib.vitro.webapp.controller.accounts.UserAccountsSelector; import edu.cornell.mannlib.vitro.webapp.controller.accounts.UserAccountsOrdering.Direction; import edu.cornell.mannlib.vitro.webapp.controller.accounts.UserAccountsOrdering.Field; @@ -39,6 +33,8 @@ public class UserAccountsSelectorTest extends AbstractTestClass { */ private static final String N3_DATA_FILENAME = "UserAccountsSelectorTest.n3"; + private static final String NS_MINE = "http://vivo.mydomain.edu/individual/"; + private static OntModel ontModel; @BeforeClass @@ -67,7 +63,6 @@ public class UserAccountsSelectorTest extends AbstractTestClass { // ---------------------------------------------------------------------- @Test(expected = NullPointerException.class) - @SuppressWarnings("unused") public void modelIsNull() { UserAccountsSelector.select(null, criteria(10, 1, DEFAULT_ORDERING, "", "")); @@ -91,7 +86,7 @@ public class UserAccountsSelectorTest extends AbstractTestClass { assertEquals("uri", "http://vivo.mydomain.edu/individual/user10", acct.getUri()); assertEquals("email", "email@jones.edu", acct.getEmailAddress()); - assertEquals("firstName", "Brian", acct.getFirstName()); + assertEquals("firstName", "Bob", acct.getFirstName()); assertEquals("lastName", "Caruso", acct.getLastName()); assertEquals("password", "garbage", acct.getMd5Password()); assertEquals("expires", 1100234965897L, acct.getPasswordLinkExpires()); @@ -196,8 +191,8 @@ public class UserAccountsSelectorTest extends AbstractTestClass { @Test public void sortByStatusAscending() { - UserAccountsOrdering orderBy = new UserAccountsOrdering( - Field.STATUS, Direction.ASCENDING); + UserAccountsOrdering orderBy = new UserAccountsOrdering(Field.STATUS, + Direction.ASCENDING); selectOnCriteria(3, 1, orderBy, "", ""); // user07 has no status: collates as least value. assertSelectedUris(10, "user07", "user01", "user04"); @@ -205,8 +200,8 @@ public class UserAccountsSelectorTest extends AbstractTestClass { @Test public void sortByStatusDescending() { - UserAccountsOrdering orderBy = new UserAccountsOrdering( - Field.STATUS, Direction.DESCENDING); + UserAccountsOrdering orderBy = new UserAccountsOrdering(Field.STATUS, + Direction.DESCENDING); selectOnCriteria(3, 1, orderBy, "", ""); assertSelectedUris(10, "user02", "user03", "user06"); } @@ -228,28 +223,75 @@ public class UserAccountsSelectorTest extends AbstractTestClass { assertSelectedUris(10, "user10", "user04", "user08"); } + // ---------------------------------------------------------------------- + // filtering tests + // ---------------------------------------------------------------------- + + @Test + public void filterAgainstRole1() { + setLoggerLevel(UserAccountsSelector.class, Level.DEBUG); + selectOnCriteria(20, 1, DEFAULT_ORDERING, NS_MINE + "role1", ""); + assertSelectedUris(6, "user01", "user02", "user03", "user05", "user06", + "user09"); + } + + @Test + public void filterAgainstNoSuchRole() { + selectOnCriteria(20, 1, DEFAULT_ORDERING, "BogusRole", ""); + assertSelectedUris(0); + } + + // ---------------------------------------------------------------------- + // search tests + // ---------------------------------------------------------------------- + + @Test + public void searchTermFoundInAllThreeFields() { + setLoggerLevel(UserAccountsSelector.class, Level.DEBUG); + selectOnCriteria(20, 1, DEFAULT_ORDERING, "", "bob"); + assertSelectedUris(3, "user02", "user05", "user10"); + } + + @Test + public void searchTermNotFound() { + setLoggerLevel(UserAccountsSelector.class, Level.DEBUG); + selectOnCriteria(20, 1, DEFAULT_ORDERING, "", "bogus"); + assertSelectedUris(0); + } + /** - * Test plan - * - *
-	 * -- searching (match against first, last, email)
-	 * app=10, pi=1, orderBy=email,A, search=bob
-	 * app=10, pi=1, orderBy=email,A, search=nomatch
-	 * 
-	 * -- filter
-	 * app=10, pi=1, orderBy=email,A, filter=role1Uri
-	 * app=10, pi=1, orderBy=email,A, filter=noSuchRole
-	 * 
-	 * -- combine
-	 * app=10, pi=1, orderBy=email,A,    search=bob, filter=role1Uri;
-	 * app=2, pi=2,  orderBy=lastName,D, search=bob, filter=role1Uri;
-	 * 
+ * If the special characters were allowed into the Regex, this would have 3 + * matches. If they are escaped properly, it will have none. */ + @Test + public void searchTermContainsSpecialRegexCharacters() { + setLoggerLevel(UserAccountsSelector.class, Level.DEBUG); + selectOnCriteria(20, 1, DEFAULT_ORDERING, "", "b.b"); + assertSelectedUris(0); + } + + // ---------------------------------------------------------------------- + // combination tests + // ---------------------------------------------------------------------- + + @Test + public void searchWithFilter() { + selectOnCriteria(20, 1, DEFAULT_ORDERING, NS_MINE + "role1", "bob"); + assertSelectedUris(2, "user02", "user05"); + } + + @Test + public void searchWithFilterPaginatedWithFunkySortOrder() { + selectOnCriteria(1, 2, new UserAccountsOrdering(Field.STATUS, + Direction.ASCENDING), NS_MINE + "role1", "bob"); + assertSelectedUris(2, "user02"); + } // ---------------------------------------------------------------------- // helper methods // ---------------------------------------------------------------------- + /** Create a new criteria object */ private UserAccountsSelectionCriteria criteria(int accountsPerPage, int pageIndex, UserAccountsOrdering orderBy, String roleFilterUri, String searchTerm) { @@ -257,6 +299,7 @@ public class UserAccountsSelectorTest extends AbstractTestClass { orderBy, roleFilterUri, searchTerm); } + /** Create a criteria object and select against it. */ private void selectOnCriteria(int accountsPerPage, int pageIndex, UserAccountsOrdering orderBy, String roleFilterUri, String searchTerm) { @@ -265,14 +308,7 @@ public class UserAccountsSelectorTest extends AbstractTestClass { selection = UserAccountsSelector.select(ontModel, criteria); } - private void assertExpectedCount(int expected) { - int actual = selection.getResultCount(); - assertEquals("count", expected, actual); - } - - /** - * Give us just the list of local names from the URIs we should expect. - */ + /** How many URIs should we expect, and which ones (local names only). */ private void assertSelectedUris(int resultCount, String... uris) { assertEquals("result count", resultCount, selection.getResultCount()); 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 d5c75faf0..85d5895c1 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 @@ -125,7 +125,7 @@ mydomain:user09 mydomain:user10 a auth:UserAccount ; auth:emailAddress "email@jones.edu" ; - auth:firstName "Brian" ; + auth:firstName "Bob" ; auth:lastName "Caruso" ; auth:md5password "garbage" ; auth:passwordChangeExpires "1100234965897" ;