NIHVIVO-2279 Implement searching and role filtering.

This commit is contained in:
j2blake 2011-05-07 21:24:03 +00:00
parent 3ad03c0767
commit 9682b91f94
4 changed files with 131 additions and 39 deletions

View file

@ -2,9 +2,11 @@
package edu.cornell.mannlib.vitro.webapp.controller.accounts; package edu.cornell.mannlib.vitro.webapp.controller.accounts;
/** /**
* On what basis are we selecting user 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 class UserAccountsSelectionCriteria {
public static final int DEFAULT_ACCOUNTS_PER_PAGE = 25; public static final int DEFAULT_ACCOUNTS_PER_PAGE = 25;

View file

@ -36,6 +36,7 @@ public class UserAccountsSelector {
private static final String PREFIX_LINES = "" private static final String PREFIX_LINES = ""
+ "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" + "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n"
+ "PREFIX fn: <http://www.w3.org/2005/xpath-functions#> \n"
+ "PREFIX auth: <http://vitro.mannlib.cornell.edu/ns/vitro/authorization#> \n"; + "PREFIX auth: <http://vitro.mannlib.cornell.edu/ns/vitro/authorization#> \n";
private static final String ALL_VARIABLES = "?uri ?email ?firstName ?lastName ?pwd ?expire ?count ?status"; 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" // + "SELECT count(DISTINCT %countVariable%) \n" //
+ "WHERE {\n" // + "WHERE {\n" //
+ " %requiredClauses% \n" // + " %requiredClauses% \n" //
+ " %optionalClauses% \n" //
+ " %filterClauses% \n" // + " %filterClauses% \n" //
+ "} \n"; + "} \n";
@ -71,6 +73,13 @@ public class UserAccountsSelector {
private static final Syntax SYNTAX = Syntax.syntaxARQ; 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. * Convenience method.
*/ */
@ -124,6 +133,7 @@ public class UserAccountsSelector {
.replace("%prefixes%", PREFIX_LINES) .replace("%prefixes%", PREFIX_LINES)
.replace("%countVariable%", COUNT_VARIABLE) .replace("%countVariable%", COUNT_VARIABLE)
.replace("%requiredClauses%", requiredClauses()) .replace("%requiredClauses%", requiredClauses())
.replace("%optionalClauses%", optionalClauses())
.replace("%filterClauses%", filterClauses()); .replace("%filterClauses%", filterClauses());
log.debug("count query: " + qString); log.debug("count query: " + qString);
@ -163,8 +173,52 @@ public class UserAccountsSelector {
} }
private String filterClauses() { private String filterClauses() {
log.warn("UserAccountsSelector.filterClauses() not implemented."); String filters = "";
return "";
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. */ /** Sort as desired, and within ties, sort by EMail address. */

View file

@ -3,9 +3,7 @@
package edu.cornell.mannlib.vitro.webapp.controller.accounts; package edu.cornell.mannlib.vitro.webapp.controller.accounts;
import static edu.cornell.mannlib.vitro.webapp.controller.accounts.UserAccountsOrdering.DEFAULT_ORDERING; 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.assertEquals;
import static org.junit.Assert.fail;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; 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.testing.AbstractTestClass;
import edu.cornell.mannlib.vitro.webapp.beans.UserAccount; 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.Direction;
import edu.cornell.mannlib.vitro.webapp.controller.accounts.UserAccountsOrdering.Field; 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 N3_DATA_FILENAME = "UserAccountsSelectorTest.n3";
private static final String NS_MINE = "http://vivo.mydomain.edu/individual/";
private static OntModel ontModel; private static OntModel ontModel;
@BeforeClass @BeforeClass
@ -67,7 +63,6 @@ public class UserAccountsSelectorTest extends AbstractTestClass {
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
@Test(expected = NullPointerException.class) @Test(expected = NullPointerException.class)
@SuppressWarnings("unused")
public void modelIsNull() { public void modelIsNull() {
UserAccountsSelector.select(null, UserAccountsSelector.select(null,
criteria(10, 1, DEFAULT_ORDERING, "", "")); criteria(10, 1, DEFAULT_ORDERING, "", ""));
@ -91,7 +86,7 @@ public class UserAccountsSelectorTest extends AbstractTestClass {
assertEquals("uri", "http://vivo.mydomain.edu/individual/user10", assertEquals("uri", "http://vivo.mydomain.edu/individual/user10",
acct.getUri()); acct.getUri());
assertEquals("email", "email@jones.edu", acct.getEmailAddress()); assertEquals("email", "email@jones.edu", acct.getEmailAddress());
assertEquals("firstName", "Brian", acct.getFirstName()); assertEquals("firstName", "Bob", acct.getFirstName());
assertEquals("lastName", "Caruso", acct.getLastName()); assertEquals("lastName", "Caruso", acct.getLastName());
assertEquals("password", "garbage", acct.getMd5Password()); assertEquals("password", "garbage", acct.getMd5Password());
assertEquals("expires", 1100234965897L, acct.getPasswordLinkExpires()); assertEquals("expires", 1100234965897L, acct.getPasswordLinkExpires());
@ -196,8 +191,8 @@ public class UserAccountsSelectorTest extends AbstractTestClass {
@Test @Test
public void sortByStatusAscending() { public void sortByStatusAscending() {
UserAccountsOrdering orderBy = new UserAccountsOrdering( UserAccountsOrdering orderBy = new UserAccountsOrdering(Field.STATUS,
Field.STATUS, Direction.ASCENDING); Direction.ASCENDING);
selectOnCriteria(3, 1, orderBy, "", ""); selectOnCriteria(3, 1, orderBy, "", "");
// user07 has no status: collates as least value. // user07 has no status: collates as least value.
assertSelectedUris(10, "user07", "user01", "user04"); assertSelectedUris(10, "user07", "user01", "user04");
@ -205,8 +200,8 @@ public class UserAccountsSelectorTest extends AbstractTestClass {
@Test @Test
public void sortByStatusDescending() { public void sortByStatusDescending() {
UserAccountsOrdering orderBy = new UserAccountsOrdering( UserAccountsOrdering orderBy = new UserAccountsOrdering(Field.STATUS,
Field.STATUS, Direction.DESCENDING); Direction.DESCENDING);
selectOnCriteria(3, 1, orderBy, "", ""); selectOnCriteria(3, 1, orderBy, "", "");
assertSelectedUris(10, "user02", "user03", "user06"); assertSelectedUris(10, "user02", "user03", "user06");
} }
@ -228,28 +223,75 @@ public class UserAccountsSelectorTest extends AbstractTestClass {
assertSelectedUris(10, "user10", "user04", "user08"); 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 * If the special characters were allowed into the Regex, this would have 3
* * matches. If they are escaped properly, it will have none.
* <pre>
* -- 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;
* </pre>
*/ */
@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 // helper methods
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
/** Create a new criteria object */
private UserAccountsSelectionCriteria criteria(int accountsPerPage, private UserAccountsSelectionCriteria criteria(int accountsPerPage,
int pageIndex, UserAccountsOrdering orderBy, String roleFilterUri, int pageIndex, UserAccountsOrdering orderBy, String roleFilterUri,
String searchTerm) { String searchTerm) {
@ -257,6 +299,7 @@ public class UserAccountsSelectorTest extends AbstractTestClass {
orderBy, roleFilterUri, searchTerm); orderBy, roleFilterUri, searchTerm);
} }
/** Create a criteria object and select against it. */
private void selectOnCriteria(int accountsPerPage, int pageIndex, private void selectOnCriteria(int accountsPerPage, int pageIndex,
UserAccountsOrdering orderBy, String roleFilterUri, UserAccountsOrdering orderBy, String roleFilterUri,
String searchTerm) { String searchTerm) {
@ -265,14 +308,7 @@ public class UserAccountsSelectorTest extends AbstractTestClass {
selection = UserAccountsSelector.select(ontModel, criteria); selection = UserAccountsSelector.select(ontModel, criteria);
} }
private void assertExpectedCount(int expected) { /** How many URIs should we expect, and which ones (local names only). */
int actual = selection.getResultCount();
assertEquals("count", expected, actual);
}
/**
* Give us just the list of local names from the URIs we should expect.
*/
private void assertSelectedUris(int resultCount, String... uris) { private void assertSelectedUris(int resultCount, String... uris) {
assertEquals("result count", resultCount, selection.getResultCount()); assertEquals("result count", resultCount, selection.getResultCount());

View file

@ -125,7 +125,7 @@ mydomain:user09
mydomain:user10 mydomain:user10
a auth:UserAccount ; a auth:UserAccount ;
auth:emailAddress "email@jones.edu" ; auth:emailAddress "email@jones.edu" ;
auth:firstName "Brian" ; auth:firstName "Bob" ;
auth:lastName "Caruso" ; auth:lastName "Caruso" ;
auth:md5password "garbage" ; auth:md5password "garbage" ;
auth:passwordChangeExpires "1100234965897" ; auth:passwordChangeExpires "1100234965897" ;