diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/accounts/manageproxies/ajax/BasicProfilesGetter.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/accounts/manageproxies/ajax/BasicProfilesGetter.java index 25de9a1f6..368f8df42 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/accounts/manageproxies/ajax/BasicProfilesGetter.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/accounts/manageproxies/ajax/BasicProfilesGetter.java @@ -2,18 +2,22 @@ package edu.cornell.mannlib.vitro.webapp.controller.accounts.manageproxies.ajax; +import static edu.cornell.mannlib.vitro.webapp.search.VitroSearchTermNames.AC_NAME_STEMMED; +import static edu.cornell.mannlib.vitro.webapp.search.VitroSearchTermNames.NAME_LOWERCASE_SINGLE_VALUED; +import static edu.cornell.mannlib.vitro.webapp.search.VitroSearchTermNames.NAME_RAW; +import static edu.cornell.mannlib.vitro.webapp.search.VitroSearchTermNames.NAME_UNSTEMMED; +import static edu.cornell.mannlib.vitro.webapp.search.VitroSearchTermNames.RDFTYPE; +import static edu.cornell.mannlib.vitro.webapp.search.VitroSearchTermNames.URI; +import static edu.cornell.mannlib.vitro.webapp.utils.solr.SolrQueryUtils.Conjunction.OR; + import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; +import javax.servlet.ServletContext; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletResponse; -import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.solr.client.solrj.SolrQuery; @@ -21,16 +25,15 @@ import org.apache.solr.client.solrj.SolrQuery.ORDER; import org.apache.solr.client.solrj.SolrServer; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.response.QueryResponse; -import org.apache.solr.common.SolrDocument; -import org.apache.solr.common.SolrDocumentList; -import org.json.JSONArray; import org.json.JSONException; import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties; import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; import edu.cornell.mannlib.vitro.webapp.controller.ajax.AbstractAjaxResponder; -import edu.cornell.mannlib.vitro.webapp.search.VitroSearchTermNames; import edu.cornell.mannlib.vitro.webapp.search.solr.SolrSetup; +import edu.cornell.mannlib.vitro.webapp.utils.solr.AutoCompleteWords; +import edu.cornell.mannlib.vitro.webapp.utils.solr.FieldMap; +import edu.cornell.mannlib.vitro.webapp.utils.solr.SolrQueryUtils; /** * Get the basic auto-complete info for the profile selection. @@ -39,63 +42,42 @@ import edu.cornell.mannlib.vitro.webapp.search.solr.SolrSetup; public class BasicProfilesGetter extends AbstractAjaxResponder { private static final Log log = LogFactory.getLog(BasicProfilesGetter.class); + private static final String WORD_DELIMITER = "[, ]+"; + private static final FieldMap RESPONSE_FIELDS = SolrQueryUtils + .fieldMap().put(URI, "uri").put(NAME_RAW, "label") + .put("bogus", "classLabel").put("bogus", "imageUrl"); + private static final String PROPERTY_PROFILE_TYPES = "proxy.eligibleTypeList"; private static final String PARAMETER_SEARCH_TERM = "term"; private static final String DEFAULT_PROFILE_TYPES = "http://www.w3.org/2002/07/owl#Thing"; private final String term; - private final List completeWords; - private final String partialWord; - private final Collection profileTypes; + private final AutoCompleteWords searchWords; + private final List profileTypes; public BasicProfilesGetter(HttpServlet servlet, VitroRequest vreq, HttpServletResponse resp) { super(servlet, vreq, resp); this.term = getStringParameter(PARAMETER_SEARCH_TERM, ""); - - List termWords = figureTermWords(); - if (termWords.isEmpty() || this.term.endsWith(" ")) { - this.completeWords = termWords; - this.partialWord = null; - } else { - this.completeWords = termWords.subList(0, termWords.size() - 1); - this.partialWord = termWords.get(termWords.size() - 1); - } + this.searchWords = SolrQueryUtils.parseForAutoComplete(term, + WORD_DELIMITER); this.profileTypes = figureProfileTypes(); log.debug(this); } - private List figureTermWords() { - List list = new ArrayList(); - String[] array = this.term.split("[, ]+"); - for (String word : array) { - String trimmed = word.trim(); - if (!trimmed.isEmpty()) { - list.add(trimmed); - } - } - return Collections.unmodifiableList(list); - } - - private Collection figureProfileTypes() { - List list = new ArrayList(); + private List figureProfileTypes() { String typesString = ConfigurationProperties.getBean(vreq).getProperty( PROPERTY_PROFILE_TYPES, DEFAULT_PROFILE_TYPES); - String[] types = typesString.split(","); - for (String type : types) { - String trimmed = type.trim(); - if (!trimmed.isEmpty()) { - list.add(trimmed); - } - } + List list = SolrQueryUtils.parseWords(typesString, + WORD_DELIMITER); if (list.isEmpty()) { log.error("No types configured for profile pages in " + PROPERTY_PROFILE_TYPES); } - return Collections.unmodifiableCollection(list); + return list; } @Override @@ -106,13 +88,15 @@ public class BasicProfilesGetter extends AbstractAjaxResponder { } try { - SolrServer solr = SolrSetup.getSolrServer(servlet - .getServletContext()); + ServletContext ctx = servlet.getServletContext(); + SolrServer solr = SolrSetup.getSolrServer(ctx); SolrQuery query = buildSolrQuery(); QueryResponse queryResponse = solr.query(query); - JSONArray jsonArray = parseResponse(queryResponse); - String response = jsonArray.toString(); + List> parsed = SolrQueryUtils + .parseResponse(queryResponse, RESPONSE_FIELDS); + + String response = assembleJsonResponse(parsed); log.debug(response); return response; } catch (SolrServerException e) { @@ -123,110 +107,20 @@ public class BasicProfilesGetter extends AbstractAjaxResponder { private SolrQuery buildSolrQuery() { SolrQuery q = new SolrQuery(); - q.setFields(VitroSearchTermNames.NAME_RAW, VitroSearchTermNames.URI); - q.setSortField(VitroSearchTermNames.NAME_LOWERCASE_SINGLE_VALUED, - ORDER.asc); - q.setFilterQueries(assembleTypeRestrictionQuery()); + q.setFields(NAME_RAW, URI); + q.setSortField(NAME_LOWERCASE_SINGLE_VALUED, ORDER.asc); + q.setFilterQueries(SolrQueryUtils.assembleConjunctiveQuery(RDFTYPE, + profileTypes, OR)); q.setStart(0); q.setRows(30); - q.setQuery(buildQueryStringFromSearchTerm()); + q.setQuery(searchWords.assembleQuery(NAME_UNSTEMMED, AC_NAME_STEMMED)); return q; - // use VitroSearchTermNames.NAME_LOWERCASE - // break the search term into words, then insert AND between the words - // TODO Auto-generated method stub - } - - private String assembleTypeRestrictionQuery() { - List terms = new ArrayList(); - for (String profileType : profileTypes) { - terms.add(VitroSearchTermNames.RDFTYPE + ":\"" + profileType + "\""); - } - String q = StringUtils.join(terms, " OR "); - log.debug("Type restriction query is '" + q + "'"); - return q; - } - - private String buildQueryStringFromSearchTerm() { - List terms = new ArrayList(); - for (String word : completeWords) { - terms.add(termForCompleteWord(word)); - } - if (partialWord != null) { - terms.add(termForPartialWord(partialWord)); - } - - String q = StringUtils.join(terms, " AND "); - log.debug("Query string is '" + q + "'"); - return q; - } - - private String termForCompleteWord(String word) { - return VitroSearchTermNames.NAME_UNSTEMMED + ":\"" + word + "\""; - } - - private String termForPartialWord(String word) { - return VitroSearchTermNames.AC_NAME_STEMMED + ":\"" + word + "\""; - } - - private JSONArray parseResponse(QueryResponse queryResponse) { - JSONArray jsonArray = new JSONArray(); - - if (queryResponse == null) { - log.error("Query response for a search was null"); - return jsonArray; - } - - SolrDocumentList docs = queryResponse.getResults(); - - if (docs == null) { - log.error("Docs for a search was null"); - return jsonArray; - } - - long hitCount = docs.getNumFound(); - log.debug("Total number of hits = " + hitCount); - if (hitCount < 1) { - return jsonArray; - } - - for (SolrDocument doc : docs) { - try { - String uri = doc.get(VitroSearchTermNames.URI).toString(); - - Object nameRaw = doc.get(VitroSearchTermNames.NAME_RAW); - String name = null; - if (nameRaw instanceof List) { - @SuppressWarnings("unchecked") - List nameRawList = (List) nameRaw; - name = nameRawList.get(0); - } else { - name = (String) nameRaw; - } - - jsonArray.put(resultRow(uri, name)); - } catch (Exception e) { - log.error("problem getting usable individuals from search " - + "hits" + e.getMessage()); - } - } - - return jsonArray; - } - - private Map resultRow(String uri, String name) { - Map map = new HashMap(); - map.put("uri", uri); - map.put("label", name); - map.put("classLabel", ""); - map.put("imageUrl", ""); - return map; } @Override public String toString() { - return "BasicProfilesGetter[term=" + term + ", completeWords=" - + completeWords + ", partialWord=" + partialWord - + ", profileTypes=" + profileTypes + "]"; + return "BasicProfilesGetter[term=" + term + ", searchWords=" + + searchWords + ", profileTypes=" + profileTypes + "]"; } } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/ajax/AbstractAjaxResponder.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/ajax/AbstractAjaxResponder.java index a2a877218..c9c8d3f71 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/ajax/AbstractAjaxResponder.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/controller/ajax/AbstractAjaxResponder.java @@ -7,6 +7,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Map; import javax.servlet.http.HttpServlet; @@ -75,6 +76,18 @@ public abstract class AbstractAjaxResponder { } } + /** + * Assemble a list of maps into a single String representing a JSON array of + * objects with fields. + */ + protected String assembleJsonResponse(List> maps) { + JSONArray jsonArray = new JSONArray(); + for (Map map: maps) { + jsonArray.put(map); + } + return jsonArray.toString(); + } + /** * AJAX responders can use a parser that extends this class. The parser must * implement "parseSolutionRow()" diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/solr/AutoCompleteWords.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/solr/AutoCompleteWords.java new file mode 100644 index 000000000..bba1ff785 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/solr/AutoCompleteWords.java @@ -0,0 +1,87 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.utils.solr; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * A helper class for use with an Auto-complete query. + * + * Any word that is followed by a delimiter is considered to be complete, and + * should be matched exactly in the query. If there is a word on the end that is + * not followed by a delimiter, it is incomplete, and should act like a + * "starts-with" query. + */ +public class AutoCompleteWords { + private static final Log log = LogFactory.getLog(AutoCompleteWords.class); + + private final String searchTerm; + private final String delimiterPattern; + private final List completeWords; + private final String partialWord; + + /** + * Package-access. Use SolrQueryUtils.parseForAutoComplete() to create an + * instance. + */ + AutoCompleteWords(String searchTerm, String delimiterPattern) { + this.searchTerm = searchTerm; + this.delimiterPattern = delimiterPattern; + + List termWords = figureTermWords(); + if (termWords.isEmpty() || this.searchTerm.endsWith(" ")) { + this.completeWords = termWords; + this.partialWord = null; + } else { + this.completeWords = termWords.subList(0, termWords.size() - 1); + this.partialWord = termWords.get(termWords.size() - 1); + } + + } + + private List figureTermWords() { + List list = new ArrayList(); + String[] array = this.searchTerm.split(this.delimiterPattern); + for (String word : array) { + String trimmed = word.trim(); + if (!trimmed.isEmpty()) { + list.add(trimmed); + } + } + return Collections.unmodifiableList(list); + } + + public String assembleQuery(String fieldNameForCompleteWords, + String fieldNameForPartialWord) { + List terms = new ArrayList(); + for (String word : this.completeWords) { + terms.add(buildTerm(fieldNameForCompleteWords, word)); + } + if (partialWord != null) { + terms.add(buildTerm(fieldNameForPartialWord, partialWord)); + } + + String q = StringUtils.join(terms, " AND "); + log.debug("Query string is '" + q + "'"); + return q; + } + + private String buildTerm(String fieldName, String word) { + return fieldName + ":\"" + word + "\""; + } + + @Override + public String toString() { + return "AutoCompleteWords[searchTerm='" + searchTerm + + "', delimiterPattern='" + delimiterPattern + + "', completeWords=" + completeWords + ", partialWord=" + + partialWord + "]"; + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/solr/FieldMap.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/solr/FieldMap.java new file mode 100644 index 000000000..877f54286 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/solr/FieldMap.java @@ -0,0 +1,41 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.utils.solr; + +import java.util.HashMap; +import java.util.Map; + +/** + * A builder object that can assemble a map of Solr field names to JSON field + * names. + * + * Use like this: + * + * m = SolrQueryUtils.fieldMap().row("this", "that").row("2nd", "row").map(); + * + */ +public class FieldMap { + private final Map m = new HashMap(); + + /** + * Add a row to the map + */ + public FieldMap put(String solrFieldName, String jsonFieldName) { + if (solrFieldName == null) { + throw new NullPointerException("solrFieldName may not be null."); + } + if (jsonFieldName == null) { + throw new NullPointerException("jsonFieldName may not be null."); + } + m.put(solrFieldName, jsonFieldName); + + return this; + } + + /** + * Release the map for use. + */ + public Map map() { + return new HashMap(m); + } +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/solr/SolrQueryUtils.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/solr/SolrQueryUtils.java new file mode 100644 index 000000000..943741158 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/solr/SolrQueryUtils.java @@ -0,0 +1,89 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.utils.solr; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang.StringUtils; +import org.apache.solr.client.solrj.response.QueryResponse; + +/** + * Some static method to help in constructing Solr queries and parsing the + * results. + */ +public class SolrQueryUtils { + public enum Conjunction { + AND, OR; + + public String joiner() { + return " " + this.name() + " "; + } + } + + /** + * Create an AutoCompleteWords object that can be used to build an + * auto-complete query. + */ + public static AutoCompleteWords parseForAutoComplete(String searchTerm, + String delimiterPattern) { + return new AutoCompleteWords(searchTerm, delimiterPattern); + } + + /** + * Create a builder object that can assemble a map of Solr field names to + * JSON field names. + */ + public static FieldMap fieldMap() { + return new FieldMap(); + } + + /** + * Parse a response into a list of maps, one map for each document. + * + * The Solr field names in the document are replaced by json field names in + * the result, according to the fieldMap. + */ + public static List> parseResponse( + QueryResponse queryResponse, FieldMap fieldMap) { + return new SolrResultsParser(queryResponse, fieldMap).parse(); + } + + /** + * Break a string into a list of words, according to a RegEx delimiter. Trim + * leading and trailing white space from each word. + */ + public static List parseWords(String typesString, + String wordDelimiter) { + List list = new ArrayList(); + String[] array = typesString.split(wordDelimiter); + for (String word : array) { + String trimmed = word.trim(); + if (!trimmed.isEmpty()) { + list.add(trimmed); + } + } + return list; + } + + /** + * Glue these words together into a query on a given field, joined by either + * AND or OR. + */ + public static String assembleConjunctiveQuery(String fieldName, + Collection words, Conjunction c) { + List terms = new ArrayList(); + for (String word : words) { + terms.add(buildTerm(fieldName, word)); + } + String q = StringUtils.join(terms, c.joiner()); + return q; + } + + private static String buildTerm(String fieldName, String word) { + return fieldName + ":\"" + word + "\""; + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/solr/SolrResultsParser.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/solr/SolrResultsParser.java new file mode 100644 index 000000000..2398c8d11 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/solr/SolrResultsParser.java @@ -0,0 +1,103 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.utils.solr; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.solr.client.solrj.response.QueryResponse; +import org.apache.solr.common.SolrDocument; +import org.apache.solr.common.SolrDocumentList; + +/** + * Parse this Solr response, creating a map of values for each document. + * + * The Solr field names in the document are replaced by json field names in the + * parsed results, according to the fieldMap. + */ +public class SolrResultsParser { + private static final Log log = LogFactory.getLog(SolrResultsParser.class); + + private final QueryResponse queryResponse; + private final Map fieldNameMapping; + + public SolrResultsParser(QueryResponse queryResponse, FieldMap fieldMap) { + this.queryResponse = queryResponse; + this.fieldNameMapping = fieldMap.map(); + } + + /** + * Parse the entire response into a list of maps. + */ + public List> parse() { + List> maps = new ArrayList>(); + + if (queryResponse == null) { + log.error("Query response for a search was null"); + return maps; + } + + SolrDocumentList docs = queryResponse.getResults(); + if (docs == null) { + log.error("Docs for a search was null"); + return maps; + } + log.debug("Total number of hits = " + docs.getNumFound()); + + for (SolrDocument doc : docs) { + maps.add(parseSingleDocument(doc)); + } + + return maps; + } + + /** + * Create a map from this document, applying translation on the field names. + */ + private Map parseSingleDocument(SolrDocument doc) { + Map result = new HashMap(); + for (String solrFieldName : fieldNameMapping.keySet()) { + String jsonFieldName = fieldNameMapping.get(solrFieldName); + + result.put(jsonFieldName, parseSingleValue(doc, solrFieldName)); + } + + return result; + } + + /** + * Find a single value in the document + */ + private String parseSingleValue(SolrDocument doc, String key) { + Object rawValue = getFirstValue(doc.get(key)); + + if (rawValue == null) { + return ""; + } + if (rawValue instanceof String) { + return (String) rawValue; + } + return String.valueOf(rawValue); + } + + /** + * The result might be a list. If so, get the first element. + */ + private Object getFirstValue(Object rawValue) { + if (rawValue instanceof List) { + List list = (List) rawValue; + if (list.isEmpty()) { + return null; + } else { + return list.get(0); + } + } else { + return rawValue; + } + } + +}