org.jasig.portal.portlets.search.SearchPortletController.java Source code

Java tutorial

Introduction

Here is the source code for org.jasig.portal.portlets.search.SearchPortletController.java

Source

/**
 * Licensed to Jasig under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Jasig licenses this file to you 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.jasig.portal.portlets.search;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;

import javax.annotation.Resource;
import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;
import javax.portlet.Event;
import javax.portlet.EventRequest;
import javax.portlet.EventResponse;
import javax.portlet.PortletRequest;
import javax.portlet.PortletSession;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang.RandomStringUtils;
import org.apache.commons.lang.Validate;
import org.jasig.portal.portlet.PortletUtils;
import org.jasig.portal.portlet.container.properties.ThemeNameRequestPropertiesManager;
import org.jasig.portal.portlet.om.IPortletWindowId;
import org.jasig.portal.portlet.registry.IPortletWindowRegistry;
import org.jasig.portal.search.PortletUrl;
import org.jasig.portal.search.PortletUrlParameter;
import org.jasig.portal.search.PortletUrlType;
import org.jasig.portal.search.SearchConstants;
import org.jasig.portal.search.SearchRequest;
import org.jasig.portal.search.SearchResult;
import org.jasig.portal.search.SearchResults;
import org.jasig.portal.url.IPortalRequestUtils;
import org.jasig.portal.url.IPortalUrlBuilder;
import org.jasig.portal.url.IPortalUrlProvider;
import org.jasig.portal.url.IPortletUrlBuilder;
import org.jasig.portal.url.UrlType;
import org.jasig.portal.utils.Tuple;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.portlet.ModelAndView;
import org.springframework.web.portlet.bind.annotation.ActionMapping;
import org.springframework.web.portlet.bind.annotation.EventMapping;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

/**
 * SearchPortletController produces both a search form and results for configured
 * search services.
 * 
 * @author Jen Bourey, jbourey@unicon.net
 * @version $Revision$
 */
@Controller
@RequestMapping("VIEW")
public class SearchPortletController {
    private static final String SEARCH_RESULTS_CACHE_NAME = SearchPortletController.class.getName()
            + ".searchResultsCache";
    private static final String SEARCH_COUNTER_NAME = SearchPortletController.class.getName() + ".searchCounter";
    private static final String SEARCH_HANDLED_CACHE_NAME = SearchPortletController.class.getName()
            + ".searchHandledCache";

    protected final Logger logger = LoggerFactory.getLogger(getClass());

    private IPortalUrlProvider portalUrlProvider;
    private IPortletWindowRegistry portletWindowRegistry;
    private IPortalRequestUtils portalRequestUtils;
    private List<IPortalSearchService> searchServices;

    // Map from result-type -> Set<tab-key>
    private Map<String, Set<String>> resultTypeMappings = Collections.emptyMap();
    private List<String> tabKeys = Collections.emptyList();
    private String defaultTabKey = "portal.results";
    private int maximumSearchesPerMinute = 18;

    @Resource(name = "searchServices")
    public void setPortalSearchServices(List<IPortalSearchService> searchServices) {
        this.searchServices = searchServices;
    }

    /**
     * The messages property key to use for the default results tab
     */
    @Value("${org.jasig.portal.portlets.searchSearchPortletController.defaultTabKey:portal.results}")
    public void setDefaultTabKey(String defaultTabKey) {
        this.defaultTabKey = defaultTabKey;
    }

    /**
     * Set the maximum number of a user can execute per minute
     */
    @Value("${org.jasig.portal.portlets.searchSearchPortletController.maximumSearchesPerMinute:18}")
    public void setMaximumSearchesPerMinute(int maximumSearchesPerMinute) {
        this.maximumSearchesPerMinute = maximumSearchesPerMinute;
    }

    /**
     * Set the mappings from TabKey to ResultType. The map keys must be strings but the values
     * can be either String or Collection<String>
     */
    @SuppressWarnings("unchecked")
    @Resource(name = "searchTabs")
    //Map of tab-key to string or collection<string> of search result types
    public void setSearchTabs(Map<String, Object> searchTabMappings) {
        final Map<String, Set<String>> resultTypeMappingsBuilder = new LinkedHashMap<String, Set<String>>();
        final List<String> tabKeysBuilder = new ArrayList<String>(searchTabMappings.size());

        for (final Map.Entry<String, Object> tabMapping : searchTabMappings.entrySet()) {
            final String tabKey = tabMapping.getKey();
            tabKeysBuilder.add(tabKey);

            final Object resultTypes = tabMapping.getValue();
            if (resultTypes instanceof Collection) {
                for (final String resultType : (Collection<String>) resultTypes) {
                    addTabKey(resultTypeMappingsBuilder, tabKey, resultType);
                }
            } else {
                final String resultType = (String) resultTypes;
                addTabKey(resultTypeMappingsBuilder, tabKey, resultType);
            }
        }

        this.resultTypeMappings = resultTypeMappingsBuilder;
        this.tabKeys = tabKeysBuilder;
    }

    protected void addTabKey(final Map<String, Set<String>> resultTypeMappingsBuilder, final String tabKey,
            final String resultType) {
        Set<String> tabKeys = resultTypeMappingsBuilder.get(resultType);
        if (tabKeys == null) {
            tabKeys = new LinkedHashSet<String>();
            resultTypeMappingsBuilder.put(resultType, tabKeys);
        }
        tabKeys.add(tabKey);
    }

    @Autowired
    public void setPortalUrlProvider(IPortalUrlProvider urlProvider) {
        this.portalUrlProvider = urlProvider;
    }

    @Autowired
    public void setPortletWindowRegistry(IPortletWindowRegistry portletWindowRegistry) {
        this.portletWindowRegistry = portletWindowRegistry;
    }

    @Autowired
    public void setPortalRequestUtils(IPortalRequestUtils portalRequestUtils) {
        Validate.notNull(portalRequestUtils);
        this.portalRequestUtils = portalRequestUtils;
    }

    @ActionMapping
    public void performSearch(@RequestParam(value = "query") String query, ActionRequest request,
            ActionResponse response) {
        final PortletSession session = request.getPortletSession();

        final String queryId = RandomStringUtils.randomAlphanumeric(32);

        Cache<String, Boolean> searchCounterCache;
        synchronized (org.springframework.web.portlet.util.PortletUtils.getSessionMutex(session)) {
            searchCounterCache = (Cache<String, Boolean>) session.getAttribute(SEARCH_COUNTER_NAME);
            if (searchCounterCache == null) {
                searchCounterCache = CacheBuilder.newBuilder().expireAfterAccess(1, TimeUnit.MINUTES)
                        .<String, Boolean>build();
                session.setAttribute(SEARCH_COUNTER_NAME, searchCounterCache);
            }
        }

        //Store the query id to track number of searches/minute
        searchCounterCache.put(queryId, Boolean.TRUE);
        if (searchCounterCache.size() > this.maximumSearchesPerMinute) {
            //Make sure old data is expired
            searchCounterCache.cleanUp();

            //Too many searches in the last minute, fail the search
            if (searchCounterCache.size() > this.maximumSearchesPerMinute) {
                response.setRenderParameter("hitMaxQueries", Boolean.TRUE.toString());
                response.setRenderParameter("query", query);
                return;
            }
        }

        // construct a new search query object from the string query
        final SearchRequest queryObj = new SearchRequest();
        queryObj.setQueryId(queryId);
        queryObj.setSearchTerms(query);

        // Create the session-shared results object
        final PortalSearchResults results = new PortalSearchResults(defaultTabKey, resultTypeMappings);

        // place the portal search results object in the session using the queryId to namespace it
        Cache<String, PortalSearchResults> searchResultsCache;
        synchronized (org.springframework.web.portlet.util.PortletUtils.getSessionMutex(session)) {
            searchResultsCache = (Cache<String, PortalSearchResults>) session
                    .getAttribute(SEARCH_RESULTS_CACHE_NAME);
            if (searchResultsCache == null) {
                searchResultsCache = CacheBuilder.newBuilder().maximumSize(20)
                        .expireAfterAccess(5, TimeUnit.MINUTES).<String, PortalSearchResults>build();
                session.setAttribute(SEARCH_RESULTS_CACHE_NAME, searchResultsCache);
            }
        }
        searchResultsCache.put(queryId, results);

        // send a search query event
        response.setEvent(SearchConstants.SEARCH_REQUEST_QNAME, queryObj);
        response.setRenderParameter("queryId", queryId);
        response.setRenderParameter("query", query);
    }

    /**
     * Performs a search of the explicitly configured {@link IPortalSearchService}s. This
     * is done as an event handler so that it can run concurrently with the other portlets
     * handling the search request
     */
    @EventMapping(SearchConstants.SEARCH_REQUEST_QNAME_STRING)
    public void handleSearchRequest(EventRequest request, EventResponse response) {
        final Event event = request.getEvent();
        final SearchRequest searchQuery = (SearchRequest) event.getValue();

        //Map used to track searches that have been handled, used so that one search doesn't get duplicate results
        ConcurrentMap<String, Boolean> searchHandledCache;
        final PortletSession session = request.getPortletSession();
        synchronized (org.springframework.web.portlet.util.PortletUtils.getSessionMutex(session)) {
            searchHandledCache = (ConcurrentMap<String, Boolean>) session.getAttribute(SEARCH_HANDLED_CACHE_NAME,
                    PortletSession.APPLICATION_SCOPE);
            if (searchHandledCache == null) {
                searchHandledCache = CacheBuilder.newBuilder().maximumSize(20)
                        .expireAfterAccess(5, TimeUnit.MINUTES).<String, Boolean>build().asMap();
                session.setAttribute(SEARCH_HANDLED_CACHE_NAME, searchHandledCache,
                        PortletSession.APPLICATION_SCOPE);
            }
        }

        final String queryId = searchQuery.getQueryId();
        if (searchHandledCache.putIfAbsent(queryId, Boolean.TRUE) != null) {
            //Already handled this search request
            return;
        }

        //Create the results
        final SearchResults results = new SearchResults();
        results.setQueryId(queryId);
        results.setWindowId(request.getWindowID());
        final List<SearchResult> searchResultList = results.getSearchResult();

        //Run the search for each service appending the results
        for (IPortalSearchService searchService : searchServices) {
            try {
                final SearchResults serviceResults = searchService.getSearchResults(request, searchQuery);
                searchResultList.addAll(serviceResults.getSearchResult());
            } catch (Exception e) {
                logger.warn(searchService.getClass() + " threw an exception when searching, it will be ignored. "
                        + searchQuery, e);
            }
        }

        //Respond with a results event if results were found
        if (!searchResultList.isEmpty()) {
            response.setEvent(SearchConstants.SEARCH_RESULTS_QNAME, results);
        }
    }

    /**
     * Handles all the SearchResults events coming back from portlets
     */
    @EventMapping(SearchConstants.SEARCH_RESULTS_QNAME_STRING)
    public void handleSearchResult(EventRequest request) {
        final Event event = request.getEvent();
        final SearchResults portletSearchResults = (SearchResults) event.getValue();

        // get the existing portal search result from the session and append
        // the results for this event
        final String queryId = portletSearchResults.getQueryId();
        final PortalSearchResults results = this.getPortalSearchResults(request, queryId);
        if (results == null) {
            this.logger.warn(
                    "No PortalSearchResults found for queryId " + queryId + ", ignoring search results: " + event);
            return;
        }

        final String windowId = portletSearchResults.getWindowId();
        final HttpServletRequest httpServletRequest = this.portalRequestUtils.getPortletHttpRequest(request);
        final IPortletWindowId portletWindowId = this.portletWindowRegistry.getPortletWindowId(httpServletRequest,
                windowId);

        //Add the other portlet's results to the main search results object
        this.addSearchResults(portletSearchResults, results, httpServletRequest, portletWindowId);
    }

    /**
     * Display a search form
     */
    @RequestMapping
    public String showSearchForm(PortletRequest request) {
        final boolean isMobile = isMobile(request);
        return isMobile ? "/jsp/Search/mobileSearch" : "/jsp/Search/search";
    }

    /**
     * Display search results
     */
    @RequestMapping(params = { "query", "queryId" })
    public ModelAndView showSearchResults(PortletRequest request, @RequestParam(value = "query") String query,
            @RequestParam(value = "queryId") String queryId) {

        final Map<String, Object> model = new HashMap<String, Object>();
        model.put("query", query);

        final PortalSearchResults portalSearchResults = this.getPortalSearchResults(request, queryId);
        final ConcurrentMap<String, List<Tuple<SearchResult, String>>> results = portalSearchResults.getResults();
        model.put("results", results);
        model.put("defaultTabKey", this.defaultTabKey);
        model.put("tabKeys", this.tabKeys);

        final boolean isMobile = isMobile(request);
        String viewName = isMobile ? "/jsp/Search/mobileSearch" : "/jsp/Search/search";

        return new ModelAndView(viewName, model);
    }

    /**
     * Display search results
     */
    @RequestMapping(params = { "query", "hitMaxQueries" })
    public ModelAndView showSearchError(PortletRequest request, @RequestParam(value = "query") String query,
            @RequestParam(value = "hitMaxQueries") boolean hitMaxQueries) {

        final Map<String, Object> model = new HashMap<String, Object>();
        model.put("query", query);
        model.put("hitMaxQueries", hitMaxQueries);

        final boolean isMobile = isMobile(request);
        String viewName = isMobile ? "/jsp/Search/mobileSearch" : "/jsp/Search/search";

        return new ModelAndView(viewName, model);
    }

    /**
     * Get the {@link PortalSearchResults} for the specified query id from the session. If there are no results null
     * is returned. 
     */
    private PortalSearchResults getPortalSearchResults(PortletRequest request, String queryId) {
        final PortletSession session = request.getPortletSession();
        @SuppressWarnings("unchecked")
        final Cache<String, PortalSearchResults> searchResultsCache = (Cache<String, PortalSearchResults>) session
                .getAttribute(SEARCH_RESULTS_CACHE_NAME);
        if (searchResultsCache == null) {
            return null;
        }

        return searchResultsCache.getIfPresent(queryId);
    }

    /**
     * @param portletSearchResults Results from a portlet
     * @param results Results collating object
     * @param httpServletRequest current request
     * @param portletWindowId Id of the portlet window that provided the results
     */
    private void addSearchResults(SearchResults portletSearchResults, PortalSearchResults results,
            final HttpServletRequest httpServletRequest, final IPortletWindowId portletWindowId) {

        for (SearchResult result : portletSearchResults.getSearchResult()) {
            final String resultUrl = this.getResultUrl(httpServletRequest, result, portletWindowId);
            this.logger.debug("Created {} with from {}", resultUrl, result.getTitle());
            results.addPortletSearchResults(resultUrl, result);
        }
    }

    /**
     * Determine the url for the search result 
     */
    protected String getResultUrl(HttpServletRequest httpServletRequest, SearchResult result,
            IPortletWindowId portletWindowId) {
        final String externalUrl = result.getExternalUrl();
        if (externalUrl != null) {
            return externalUrl;
        }

        UrlType urlType = UrlType.RENDER;

        final PortletUrl portletUrl = result.getPortletUrl();
        if (portletUrl != null) {
            final PortletUrlType type = portletUrl.getType();
            if (type != null) {
                switch (type) {
                case ACTION: {
                    urlType = UrlType.ACTION;
                    break;
                }
                default:
                case RENDER: {
                    urlType = UrlType.RENDER;
                    break;
                }
                case RESOURCE: {
                    urlType = UrlType.RESOURCE;
                    break;
                }
                }
            }
        }

        final IPortalUrlBuilder portalUrlBuilder = this.portalUrlProvider
                .getPortalUrlBuilderByPortletWindow(httpServletRequest, portletWindowId, urlType);
        final IPortletUrlBuilder portletUrlBuilder = portalUrlBuilder.getTargetedPortletUrlBuilder();

        if (portletUrl != null) {
            final String portletMode = portletUrl.getPortletMode();
            if (portletMode != null) {
                portletUrlBuilder.setPortletMode(PortletUtils.getPortletMode(portletMode));
            }
            final String windowState = portletUrl.getWindowState();
            if (windowState != null) {
                portletUrlBuilder.setWindowState(PortletUtils.getWindowState(windowState));
            }
            for (final PortletUrlParameter param : portletUrl.getParam()) {
                final String name = param.getName();
                final List<String> values = param.getValue();
                portletUrlBuilder.addParameter(name, values.toArray(new String[values.size()]));
            }
        }

        return portalUrlBuilder.getUrlString();
    }

    public boolean isMobile(PortletRequest request) {
        String themeName = request.getProperty(ThemeNameRequestPropertiesManager.THEME_NAME_PROPERTY);
        return "UniversalityMobile".equals(themeName);
    }
}