edu.cornell.mannlib.vitro.webapp.search.controller.AutocompleteController.java Source code

Java tutorial

Introduction

Here is the source code for edu.cornell.mannlib.vitro.webapp.search.controller.AutocompleteController.java

Source

/* $This file is distributed under the terms of the license in /doc/license.txt$ */

package edu.cornell.mannlib.vitro.webapp.search.controller;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
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.json.JSONArray;
import org.json.JSONObject;

import edu.cornell.mannlib.vitro.webapp.application.ApplicationUtils;
import edu.cornell.mannlib.vitro.webapp.auth.permissions.SimplePermission;
import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.AuthorizationRequest;
import edu.cornell.mannlib.vitro.webapp.beans.VClass;
import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest;
import edu.cornell.mannlib.vitro.webapp.controller.ajax.VitroAjaxController;
import edu.cornell.mannlib.vitro.webapp.dao.VClassDao;
import edu.cornell.mannlib.vitro.webapp.modules.searchEngine.SearchEngine;
import edu.cornell.mannlib.vitro.webapp.modules.searchEngine.SearchQuery;
import edu.cornell.mannlib.vitro.webapp.modules.searchEngine.SearchResponse;
import edu.cornell.mannlib.vitro.webapp.modules.searchEngine.SearchResultDocument;
import edu.cornell.mannlib.vitro.webapp.modules.searchEngine.SearchResultDocumentList;
import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService;
import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.RDFServiceUtils;
import edu.cornell.mannlib.vitro.webapp.search.VitroSearchTermNames;

/**
 * AutocompleteController generates autocomplete content
 * via the search index.
 */

public class AutocompleteController extends VitroAjaxController {

    private static final long serialVersionUID = 1L;
    private static final Log log = LogFactory.getLog(AutocompleteController.class);

    //private static final String TEMPLATE_DEFAULT = "autocompleteResults.ftl";

    private static final String PARAM_QUERY = "term";
    private static final String PARAM_RDFTYPE = "type";
    private static final String PARAM_MULTIPLE_RDFTYPE = "multipleTypes";

    private boolean hasMultipleTypes = false;

    String NORESULT_MSG = "";
    private static final int DEFAULT_MAX_HIT_COUNT = 1000;

    public static final int MAX_QUERY_LENGTH = 500;

    @Override
    protected AuthorizationRequest requiredActions(VitroRequest vreq) {
        return SimplePermission.USE_BASIC_AJAX_CONTROLLERS.ACTION;
    }

    @Override
    protected void doRequest(VitroRequest vreq, HttpServletResponse response) throws IOException, ServletException {

        try {
            String qtxt = vreq.getParameter(PARAM_QUERY);
            String typeParam = vreq.getParameter(PARAM_RDFTYPE);
            if (typeParam != null) {
                String[] parts = typeParam.split(",");
                if (parts.length > 1) {
                    hasMultipleTypes = true;
                } else if (parts.length == 1) {
                    String askQuery = "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n"
                            + "ASK { ?something rdfs:subClassOf <" + typeParam.replace(",", "") + "> }";
                    if (getRdfService(vreq).sparqlAskQuery(askQuery)) {
                        hasMultipleTypes = true;
                    }
                }
            } else {
                //if the type parameter is null, no range is specified and individuals of any class might be returned
                //in this case, it would be useful to show the most specific type of the individual
                hasMultipleTypes = true;
            }

            SearchQuery query = getQuery(qtxt, vreq);
            if (query == null) {
                log.debug("query for '" + qtxt + "' is null.");
                doNoQuery(response);
                return;
            }
            log.debug("query for '" + qtxt + "' is " + query.toString());

            SearchEngine search = ApplicationUtils.instance().getSearchEngine();
            SearchResponse queryResponse = search.query(query);

            if (queryResponse == null) {
                log.error("Query response for a search was null");
                doNoSearchResults(response);
                return;
            }

            SearchResultDocumentList docs = queryResponse.getResults();

            if (docs == null) {
                log.error("Docs for a search was null");
                doNoSearchResults(response);
                return;
            }

            long hitCount = docs.getNumFound();
            log.debug("Total number of hits = " + hitCount);
            if (hitCount < 1) {
                doNoSearchResults(response);
                return;
            }

            List<SearchResult> results = new ArrayList<SearchResult>();
            for (SearchResultDocument doc : docs) {
                try {
                    String uri = doc.getStringValue(VitroSearchTermNames.URI);
                    String name = doc.getStringValue(VitroSearchTermNames.NAME_RAW);
                    //There may be multiple most specific types, sending them all back
                    String mst = doc.getStringValue(VitroSearchTermNames.MOST_SPECIFIC_TYPE_URIS);
                    //Assuming these will get me string values
                    Collection<Object> mstObjValues = doc
                            .getFieldValues(VitroSearchTermNames.MOST_SPECIFIC_TYPE_URIS);
                    String[] mstStringValues = mstObjValues.toArray(new String[mstObjValues.size()]);
                    List<String> mstValues = Arrays.asList(mstStringValues);
                    SearchResult result = new SearchResult(name, uri, mst, mstValues, hasMultipleTypes, vreq);
                    results.add(result);
                    log.debug("results = " + results.toString());
                } catch (Exception e) {
                    log.error("problem getting usable individuals from search " + "hits" + e.getMessage());
                }
            }
            // now that we have the search result, reset this boolean
            hasMultipleTypes = false;

            Collections.sort(results);

            JSONArray jsonArray = new JSONArray();
            for (SearchResult result : results) {
                //jsonArray.put(result.toMap());
                jsonArray.put(result.toJSONObject());
            }
            response.getWriter().write(jsonArray.toString());

        } catch (Throwable e) {
            log.error(e, e);
            doSearchError(response);
        }
    }

    private SearchQuery getQuery(String queryStr, VitroRequest vreq) {

        if (queryStr == null) {
            log.error("There was no parameter '" + PARAM_QUERY + "' in the request.");
            return null;
        } else if (queryStr.length() > MAX_QUERY_LENGTH) {
            log.debug("The search was too long. The maximum " + "query length is " + MAX_QUERY_LENGTH);
            return null;
        }

        SearchQuery query = ApplicationUtils.instance().getSearchEngine().createQuery();
        query.setStart(0).setRows(DEFAULT_MAX_HIT_COUNT);
        setNameQuery(query, queryStr, vreq);
        // Filter by type
        String typeParam = vreq.getParameter(PARAM_RDFTYPE);
        String multipleTypesParam = vreq.getParameter(PARAM_MULTIPLE_RDFTYPE);

        if (typeParam != null) {
            addFilterQuery(query, typeParam, multipleTypesParam);
        }

        query.addFields(VitroSearchTermNames.NAME_RAW, VitroSearchTermNames.URI,
                VitroSearchTermNames.MOST_SPECIFIC_TYPE_URIS); // fields to retrieve

        // Can't sort on multivalued field, so we sort the results in Java when we get them.
        // query.addSortField(VitroSearchTermNames.NAME_LOWERCASE, Order.ASC);

        return query;
    }

    private void addFilterQuery(SearchQuery query, String typeParam, String multipleTypesParam) {
        if (multipleTypesParam == null || multipleTypesParam.equals("null") || multipleTypesParam.isEmpty()) {
            //Single type parameter, process as usual
            query.addFilterQuery(VitroSearchTermNames.RDFTYPE + ":\"" + typeParam + "\"");
        } else {
            //Types should be comma separated
            String[] typeParams = typeParam.split(",");
            int len = typeParams.length;
            int i;
            List<String> filterQueries = new ArrayList<String>();

            for (i = 0; i < len; i++) {
                filterQueries.add(VitroSearchTermNames.RDFTYPE + ":\"" + typeParams[i] + "\" ");
            }
            String filterQuery = StringUtils.join(filterQueries, " OR ");
            query.addFilterQuery(filterQuery);
        }
    }

    private void setNameQuery(SearchQuery query, String queryStr, HttpServletRequest request) {

        if (StringUtils.isBlank(queryStr)) {
            log.error("No query string");
        }
        String tokenizeParam = request.getParameter("tokenize");
        boolean tokenize = "true".equals(tokenizeParam);

        // Note: Stemming is only relevant if we are tokenizing: an untokenized name
        // query will not be stemmed. So we don't look at the stem parameter until we get to
        // setTokenizedNameQuery().
        if (tokenize) {
            setTokenizedNameQuery(query, queryStr, request);
        } else {
            setUntokenizedNameQuery(query, queryStr);
        }
    }

    private void setTokenizedNameQuery(SearchQuery query, String queryStr, HttpServletRequest request) {

        /* We currently have no use case for a tokenized, unstemmed autocomplete search field, so the option
         * has been disabled. If needed in the future, will need to add a new field and field type which
         * is like AC_NAME_STEMMED but doesn't include the stemmer.
        String stemParam = (String) request.getParameter("stem");
        boolean stem = "true".equals(stemParam);
        if (stem) {
        String acTermName = VitroSearchTermNames.AC_NAME_STEMMED;
        String nonAcTermName = VitroSearchTermNames.NAME_STEMMED;
        } else {
        String acTermName = VitroSearchTermNames.AC_NAME_UNSTEMMED;
        String nonAcTermName = VitroSearchTermNames.NAME_UNSTEMMED;
        }
        */

        String acTermName = VitroSearchTermNames.AC_NAME_STEMMED;
        String nonAcTermName = VitroSearchTermNames.NAME_STEMMED;
        String acQueryStr;

        if (queryStr.endsWith(" ")) {
            acQueryStr = makeTermQuery(nonAcTermName, queryStr, true);
        } else {
            int indexOfLastWord = queryStr.lastIndexOf(" ") + 1;
            List<String> terms = new ArrayList<String>(2);

            String allButLastWord = queryStr.substring(0, indexOfLastWord);
            if (StringUtils.isNotBlank(allButLastWord)) {
                terms.add(makeTermQuery(nonAcTermName, allButLastWord, true));
            }

            String lastWord = queryStr.substring(indexOfLastWord);
            if (StringUtils.isNotBlank(lastWord)) {
                terms.add(makeTermQuery(acTermName, lastWord, false));
            }

            acQueryStr = StringUtils.join(terms, " AND ");
        }

        log.debug("Tokenized name query string = " + acQueryStr);
        query.setQuery(acQueryStr);

    }

    private void setUntokenizedNameQuery(SearchQuery query, String queryStr) {
        queryStr = queryStr.trim();
        queryStr = makeTermQuery(VitroSearchTermNames.AC_NAME_UNTOKENIZED, queryStr, true);
        query.setQuery(queryStr);
    }

    private String makeTermQuery(String term, String queryStr, boolean mayContainWhitespace) {
        if (mayContainWhitespace) {
            queryStr = "\"" + escapeWhitespaceInQueryString(queryStr) + "\"";
        }
        return term + ":" + queryStr;
    }

    private String escapeWhitespaceInQueryString(String queryStr) {
        // The search engine wants whitespace to be escaped with a backslash
        return queryStr.replaceAll("\\s+", "\\\\ ");
    }

    private void doNoQuery(HttpServletResponse response) throws IOException {
        // For now, we are not sending an error message back to the client because
        // with the default autocomplete configuration it chokes.
        doNoSearchResults(response);
    }

    private void doSearchError(HttpServletResponse response) throws IOException {
        // For now, we are not sending an error message back to the client because 
        // with the default autocomplete configuration it chokes.
        doNoSearchResults(response);
    }

    private void doNoSearchResults(HttpServletResponse response) throws IOException {
        response.getWriter().write("[]");
    }

    private RDFService getRdfService(HttpServletRequest req) {
        return RDFServiceUtils.getRDFService(new VitroRequest(req));
    }

    public class SearchResult implements Comparable<Object> {
        private String label;
        private String uri;
        private String msType;
        private List<String> allMsTypes;
        private boolean hasMultipleTypes;

        SearchResult(String label, String uri, String msType, List<String> allMsTypes, boolean hasMultipleTypes,
                VitroRequest vreq) {
            if (hasMultipleTypes) {
                this.label = label + " (" + getMsTypeLocalName(msType, vreq) + ")";
            } else {
                this.label = label;
            }
            this.uri = uri;
            this.msType = msType;
            this.allMsTypes = allMsTypes;
        }

        public String getLabel() {
            return label;
        }

        public String getJsonLabel() {
            return JSONObject.quote(label);
        }

        public String getUri() {
            return uri;
        }

        public String getJsonUri() {
            return JSONObject.quote(uri);
        }

        public String getMsType() {
            return msType;
        }

        public List<String> getAllMsTypes() {
            return allMsTypes;
        }

        public String getMsTypeLocalName(String theUri, VitroRequest vreq) {
            VClassDao vcDao = vreq.getUnfilteredAssertionsWebappDaoFactory().getVClassDao();
            VClass vClass = vcDao.getVClassByURI(theUri);
            String theType = ((vClass.getName() == null) ? "" : vClass.getName());
            return theType;
        }

        public String getJsonMsType() {
            return JSONObject.quote(msType);
        }

        //Simply passing in the array in the map converts it to a string and not to an array
        //which is what we want so need to convert to an object instad
        JSONObject toJSONObject() {
            JSONObject jsonObj = new JSONObject();
            try {
                jsonObj.put("label", label);
                jsonObj.put("uri", uri);
                //Leaving this in for now, in case there is code out there that depends on this single string version
                //But this should really be changed so that the entire array is all that should be returned
                jsonObj.put("msType", msType);
                //map.put("allMsTypes", allMsTypes);
                JSONArray allMsTypesArray = new JSONArray(allMsTypes);
                jsonObj.put("allMsTypes", allMsTypesArray);
            } catch (Exception ex) {
                log.error("Error occurred in converting values to JSON object", ex);
            }
            return jsonObj;
        }

        public int compareTo(Object o) throws ClassCastException {
            if (!(o instanceof SearchResult)) {
                throw new ClassCastException("Error in SearchResult.compareTo(): expected SearchResult object.");
            }
            SearchResult sr = (SearchResult) o;
            return label.compareToIgnoreCase(sr.getLabel());
        }
    }

}