/* * $HeadURL$ * $Id$ * * Copyright (c) 2006-2010 by Public Library of Science * http://plos.org * http://ambraproject.org * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.ambraproject.action.search; import org.ambraproject.service.user.UserService; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Required; import org.springframework.web.context.request.ServletRequestAttributes; import org.ambraproject.ApplicationException; import org.ambraproject.action.BaseSessionAwareActionSupport; import org.ambraproject.service.search.SearchParameters; import org.ambraproject.views.SearchResultSinglePage; import org.ambraproject.views.SearchHit; import org.ambraproject.service.search.SearchService; import org.ambraproject.freemarker.AmbraFreemarkerConfig; import org.ambraproject.web.VirtualJournalContext; import javax.servlet.http.HttpServletRequest; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * Manage the user interactions for all searches. */ @SuppressWarnings("serial") public class SearchAction extends BaseSearchAction { private static final Logger log = LoggerFactory.getLogger(SearchAction.class); private static final String CONFIG_DOI_RESOLVER_URL = "ambra.services.crossref.plos.doiurl"; private static final int RECENT_SEARCHES_NUMBER_TO_SHOW = 5; private static final int MAX_FILTERS_SHOWN = 50; // Flag telling this action whether or not the search should be executed. private String noSearchFlag; private UserService userService; private AmbraFreemarkerConfig ambraFreemarkerConfig; // Used for display of search results private Collection<SearchHit> searchResults; private String queryAsExecuted; protected String searchType; // Creates list of all searchable Journals for display private List<Map> journals; private List<Map> subjects; private List<Map> articleTypes; private boolean filterReset = false; // Used when this Action class redirects the user the Article main page. private String articleURI; private String journalURL; /** * @return return simple search result */ public String executeSimpleSearch() throws ApplicationException { searchType = "simple"; setDefaultSearchParams(); final String queryString = getSearchParameters().getQuery(); if (StringUtils.isBlank(queryString) || queryString.equals("Search articles...")) { addFieldError("query", "Please enter a search query."); setQuery(""); return INPUT; } else { SearchParameters params = getSearchParameters(); resultsSinglePage = searchService.simpleSearch(params); //TODO: take out these intermediary objects and pass "SearchResultSinglePage" to the FTL searchResults = resultsSinglePage.getHits(); queryAsExecuted = resultsSinglePage.getQueryAsExecuted(); //If page size is zero, assume totalPages is zero int totPages = (getPageSize() == 0)?0:((getTotalNoOfResults() + getPageSize() - 1) / getPageSize()); setStartPage(Math.max(0, Math.min(getStartPage(), totPages - 1))); // Recent Searches must have both a Request URI and a Request Query String, else the URL is useless. if (doSearch() && getRequestURL() != null && getRequestQueryString() != null) { addRecentSearch(queryString, getRequestURL() + "?" + getRequestQueryString()); } if(getCurrentUser() != null) { userService.recordUserSearch(getCurrentUser().getID(), params.getQuery(), params.toString()); } return SUCCESS; } } /** * @return return a search result based on the <code>unformattedSearch</code> parameter, * moderated by filters based on the and the journal and category properties. */ public String executeUnformattedSearch() { searchType = "unformatted"; setDefaultSearchParams(); if ( ! doSearch()) { try { setFiltersData(); } catch (ApplicationException e) { addActionError("Search failed"); log.error("Querying for search meta data has failed: ", e); return ERROR; } return INPUT; } if (StringUtils.isBlank(getSearchParameters().getUnformattedQuery())) { addFieldError("unfilteredQuery", "Please enter a search query."); try { setFiltersData(); } catch (ApplicationException e) { addActionError("Search failed"); log.error("Querying for search meta data has failed: ", e); return ERROR; } return INPUT; } else { try { SearchParameters params = getSearchParameters(); resultsSinglePage = searchService.advancedSearch(params); //TODO: take out these intermediary objects and pass "SearchResultSinglePage" to the FTL searchResults = resultsSinglePage.getHits(); queryAsExecuted = resultsSinglePage.getQueryAsExecuted(); int totPages = (getTotalNoOfResults() + getPageSize() - 1) / getPageSize(); setStartPage(Math.max(0, Math.min(getStartPage(), totPages - 1))); // Recent Searches must have both a Request URI and a Request Query String, else the URL is useless. if (doSearch() && getRequestURL() != null && getRequestQueryString() != null) { addRecentSearch(queryAsExecuted, getRequestURL() + "?" + getRequestQueryString()); } if(getCurrentUser() != null) { userService.recordUserSearch(getCurrentUser().getID(), params.getQuery(), params.toString()); } } catch (ApplicationException e) { addActionError("Search failed"); log.error("Search failed for the query string: " + getSearchParameters().getUnformattedQuery(), e); return ERROR; } return SUCCESS; } } /** * @return return a search result based on the <code>unformattedSearch</code> parameter, * moderated by filters based on the and the journal and category properties. */ public String executeQuickSearch() { searchType = "quickSearch"; setDefaultSearchParams(); if ( ! doSearch()) { try { //filterJournals are NOT set in form in global_header.ftl setFiltersData(); } catch (ApplicationException e) { addActionError("Search failed"); log.error("Querying for search meta data has failed: ", e); return ERROR; } return INPUT; } if (StringUtils.isBlank(getSearchParameters().getVolume()) && StringUtils.isBlank(getSearchParameters().getELocationId()) && StringUtils.isBlank(getSearchParameters().getId())) { addFieldError("volume", "Please enter at least one search term."); try { //filterJournals are NOT set in form in global_header.ftl setFiltersData(); } catch (ApplicationException e) { addActionError("Search failed"); log.error("Querying for search meta data has failed: ", e); return ERROR; } return INPUT; } try { SearchParameters params = getSearchParameters(); resultsSinglePage = searchService.findAnArticleSearch(params); // If only ONE result, then send the user to fetchArticle.action for that article if (resultsSinglePage.getTotalNoOfResults() == 1) { articleURI = "info:doi/" + resultsSinglePage.getHits().get(0).getUri(); journalURL = ambraFreemarkerConfig.getJournalUrlFromIssn(resultsSinglePage.getHits().get(0).getIssn()); if (journalURL == null || journalURL.trim().length() < 1) { journalURL = configuration.getString(CONFIG_DOI_RESOLVER_URL, ""); } // add request context to the url VirtualJournalContext vjc = this.getVirtualJournalContext(); String rc = vjc.getRequestContext(); if (rc != null && rc.trim().length() > 0) { journalURL = journalURL + rc; } return "redirectToArticle"; // Tells struts.xml to send the user to fetchArticle.action } //TODO: take out these intermediary objects and pass "SearchResultSinglePage" to the FTL searchResults = resultsSinglePage.getHits(); queryAsExecuted = resultsSinglePage.getQueryAsExecuted(); int totPages = (getTotalNoOfResults() + getPageSize() - 1) / getPageSize(); setStartPage(Math.max(0, Math.min(getStartPage(), totPages - 1))); // Recent Searches must have both a Request URI and a Request Query String, else the URL is useless. if (doSearch() && getRequestURL() != null && getRequestQueryString() != null) { addRecentSearch(queryAsExecuted, getRequestURL() + "?" + getRequestQueryString()); } if(getCurrentUser() != null) { userService.recordUserSearch(getCurrentUser().getID(), params.getQuery(), params.toString()); } } catch (ApplicationException e) { addActionError("Search failed"); log.error("Search failed for the findAnArticle query using the SearchParameters object: " + getSearchParameters().toString(), e); return ERROR; } return SUCCESS; } /** * If no search is performed, we need some data to populate the drop down and select lists * If no results are found, reset the filters and try query again * @throws ApplicationException */ protected void setFiltersData() throws ApplicationException { //Eventually we'll want to migrate this action to use a database to get this data //But for now, SOLR seems the best place SearchResultSinglePage sr = searchService.getFilterData(getSearchParameters()); journals = sr.getJournalFacet(); subjects = sr.getSubjectFacet(); if(subjects != null && subjects.size() > MAX_FILTERS_SHOWN) { subjects = subjects.subList(0,MAX_FILTERS_SHOWN); } articleTypes = sr.getArticleTypeFacet(); if(articleTypes != null && articleTypes.size() > MAX_FILTERS_SHOWN) { articleTypes = articleTypes.subList(0,MAX_FILTERS_SHOWN); } filterReset = sr.getFiltersReset(); } // Getters and Setters that belong to this Action /** * @return the noSearchFlag */ public String getNoSearchFlag() { return noSearchFlag; } /** * Have the filters been removed for existing query? * @return true if the filter has been reset */ public boolean getFilterReset() { return filterReset; } /** * Which source of the query performed. Controls which search form and display methodology to use. * @return The type of search that was performed */ public String getSearchType() { return searchType; } /** * The query String that was used to get the search results. * @return The actual query that was perfomed */ public String getQueryAsExecuted() { return queryAsExecuted; } /** * @param noSearchFlag the noSearchFlag to set */ public void setNoSearchFlag(String noSearchFlag) { this.noSearchFlag = noSearchFlag; } private boolean doSearch() { return noSearchFlag == null; } /** * @return the search results. */ public Collection<SearchHit> getSearchResults() { return searchResults; } public List getSorts() { return searchService.getSorts(); } /** * A list of journals that the current search results appear in * @return a journals List and frequency count */ public List<Map> getJournals() { return journals; } /** * A list of all the subjects that appear in the current search results * @return a subject list and frequency count */ public List<Map> getSubjects() { return subjects; } /** * A list of all the article types that appear in the current search results * @return an article type list and frequency count */ public List<Map> getArticleTypes() { return articleTypes; } /** * Set the userService * * @param userService userService */ @Required public void setUserService(final UserService userService) { this.userService = userService; } /** * Set the config class containing all of the properties used by the Freemarker templates so * those values can be used within this Action class. * @param ambraFreemarkerConfig All of the configuration properties used by the Freemarker templates */ @Required public void setAmbraFreemarkerConfig(final AmbraFreemarkerConfig ambraFreemarkerConfig) { this.ambraFreemarkerConfig = ambraFreemarkerConfig; } /** * The URI of one article. * Used when this Action class redirects the user the Article main page. * @return the URI of one article */ public String getArticleURI() { return articleURI; } /** * The URL for the Journal in which <code>articleURI</code> is published. * This is necessary for properly constructing a URL for <code>articleURI</code> because Ambra * does not automatically redirect the user to articles in other Journals. * @return The URL for the Journal in which <code>articleURI</code> is published */ public String getJournalURL() { return journalURL; } /** * From the HTTP Session, get the searches performed by this user. * Each element in the returned Map has a key which is the link text (to be displayed to the user) * and a value which is the URL of that link. * @return The searches performed by this user */ public LinkedHashMap<String, String> getRecentSearches() { return super.getRecentSearches(); } /** * Add a new Recent Search to the Map of Recent Searches stored in HTTP Session Scope. * If there are at least RECENT_SEARCHES_NUMBER_TO_SHOW elements in the Map, * then remove the oldest element before adding a new element. * If the element with the key <code>displayText</code> already exists, then remove that element * before adding the new recent search. * * @param displayText The link text which will be displayed to the user * @param url The URL which will be executed when the user clicks on the displayText */ protected void addRecentSearch(String displayText, String url) { // If either parameter is bogus, then fail silently. if (displayText == null || displayText.trim().length() < 1 || url == null || url.trim().length() < 1) { log.warn("Unable to add to Recent Searches (in Session Scope) the key/value pair: displayText = \'" + displayText + "\' and url = \'" + url + "\'"); return; } if (getRecentSearches().containsKey(displayText)) { getRecentSearches().remove(displayText); } else if (getRecentSearches().size() >= RECENT_SEARCHES_NUMBER_TO_SHOW) { getRecentSearches().remove(getRecentSearches().keySet().iterator().next()); // Remove the first element. } getRecentSearches().put(displayText, url); } /** * Get the URL from the submitted HttpServletRequest. * @return The URL submitted, up to the question mark */ private String getRequestURL() { return getHttpServletRequest().getRequestURL().toString(); } /** * Get the Query String from the submitted HttpServletRequest. * @return The Query String submitted, meaning the part of the URL after the question mark */ private String getRequestQueryString() { return getHttpServletRequest().getQueryString(); } /** * Convenience method allowing direct access to the HttpServletRequest which was just submitted * * TODO: replace this method of accessing the HTTP Request Attributes with something less ugly. Action classes should only have to deal with POJOs. * * @return The HttpServletRequest which was just submitted */ private HttpServletRequest getHttpServletRequest() { return ((ServletRequestAttributes)(requestAttributes.get( "org.springframework.web.context.request.RequestContextListener.REQUEST_ATTRIBUTES"))) .getRequest(); } }