/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.xmlui.aspect.discovery;
import org.apache.cocoon.caching.CacheableProcessingComponent;
import org.apache.cocoon.environment.ObjectModelHelper;
import org.apache.cocoon.environment.Request;
import org.apache.cocoon.util.HashUtil;
import org.apache.commons.lang.StringUtils;
import org.apache.excalibur.source.SourceValidity;
import org.apache.log4j.Logger;
import org.dspace.app.xmlui.cocoon.AbstractDSpaceTransformer;
import org.dspace.app.xmlui.utils.DSpaceValidity;
import org.dspace.app.xmlui.utils.HandleUtil;
import org.dspace.app.xmlui.utils.UIException;
import org.dspace.app.xmlui.wing.Message;
import org.dspace.app.xmlui.wing.WingException;
import org.dspace.app.xmlui.wing.element.List;
import org.dspace.app.xmlui.wing.element.Options;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.DSpaceObject;
import org.dspace.core.Context;
import org.dspace.core.LogManager;
import org.dspace.discovery.*;
import org.dspace.discovery.configuration.DiscoveryConfiguration;
import org.dspace.discovery.configuration.DiscoveryConfigurationParameters;
import org.dspace.discovery.configuration.DiscoverySearchFilterFacet;
import org.dspace.handle.HandleManager;
import org.dspace.utils.DSpace;
import org.xml.sax.SAXException;
import java.io.IOException;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.sql.SQLException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Renders the sidebar filters in Discovery
*
* @author Kevin Van de Velde (kevin at atmire dot com)
* @author Mark Diggory (markd at atmire dot com)
* @author Ben Bosman (ben at atmire dot com)
*/
public class SidebarFacetsTransformer extends AbstractDSpaceTransformer implements CacheableProcessingComponent {
private static final Logger log = Logger.getLogger(SidebarFacetsTransformer.class);
/**
* Cached query results
*/
protected DiscoverResult queryResults;
/**
* Cached query arguments
*/
protected DiscoverQuery queryArgs;
/**
* Cached validity object
*/
protected SourceValidity validity;
private static final Message T_FILTER_HEAD = message("xmlui.discovery.AbstractFiltersTransformer.filters.head");
private static final Message T_VIEW_MORE = message("xmlui.discovery.AbstractFiltersTransformer.filters.view-more");
protected SearchService getSearchService()
{
DSpace dspace = new DSpace();
org.dspace.kernel.ServiceManager manager = dspace.getServiceManager() ;
return manager.getServiceByName(SearchService.class.getName(),SearchService.class);
}
/**
* Generate the unique caching key.
* This key must be unique inside the space of this component.
*/
public Serializable getKey() {
try {
DSpaceObject dso = HandleUtil.obtainHandle(objectModel);
if (dso != null)
{
return HashUtil.hash(dso.getHandle());
}else{
return "0";
}
}
catch (SQLException sqle) {
// Ignore all errors and just return that the component is not
// cachable.
return "0";
}
}
/**
* Generate the cache validity object.
* <p/>
* The validity object will include the collection being viewed and
* all recently submitted items. This does not include the community / collection
* hierarchy, when this changes they will not be reflected in the cache.
*/
public SourceValidity getValidity() {
if (this.validity == null) {
try {
DSpaceObject dso = HandleUtil.obtainHandle(objectModel);
DSpaceValidity val = new DSpaceValidity();
// Retrieve any facet results to add to the validity key
performSearch();
// Add the actual collection;
if (dso != null)
{
val.add(dso);
}
val.add("numFound:" + queryResults.getDspaceObjects().size());
for (DSpaceObject resultDso : queryResults.getDspaceObjects()) {
val.add(resultDso);
}
for (String facetField : queryResults.getFacetResults().keySet()) {
val.add(facetField);
java.util.List<DiscoverResult.FacetResult> facetValues = queryResults.getFacetResults().get(facetField);
for (DiscoverResult.FacetResult facetValue : facetValues) {
val.add(facetField + facetValue.getAsFilterQuery() + facetValue.getCount());
}
}
this.validity = val.complete();
}
catch (Exception e) {
log.error(e.getMessage(),e);
}
//TODO: dependent on tags as well :)
}
return this.validity;
}
public void performSearch() throws SearchServiceException, UIException, SQLException {
DSpaceObject dso = getScope();
Request request = ObjectModelHelper.getRequest(objectModel);
queryArgs = getQueryArgs(context, dso, DiscoveryUIUtils.getFilterQueries(request, context));
//If we are on a search page performing a search a query may be used
String query = request.getParameter("query");
if(query != null && !"".equals(query)){
queryArgs.setQuery(query);
}
//We do not need to retrieve any dspace objects, only facets
queryArgs.setMaxResults(0);
queryResults = getSearchService().search(context, dso, queryArgs);
}
@Override
public void addOptions(Options options) throws SAXException, WingException, SQLException, IOException, AuthorizeException {
Request request = ObjectModelHelper.getRequest(objectModel);
try {
performSearch();
}catch (Exception e){
log.error("Error while searching for sidebar facets", e);
return;
}
if (this.queryResults != null) {
DSpaceObject dso = HandleUtil.obtainHandle(objectModel);
java.util.List<String> fqs = Arrays.asList(DiscoveryUIUtils.getFilterQueries(request, context));
DiscoveryConfiguration discoveryConfiguration = SearchUtils.getDiscoveryConfiguration(dso);
java.util.List<DiscoverySearchFilterFacet> facets = discoveryConfiguration.getSidebarFacets();
if (facets != null && 0 < facets.size()) {
List browse = null;
for (DiscoverySearchFilterFacet field : facets) {
//Retrieve our values
java.util.List<DiscoverResult.FacetResult> facetValues = queryResults.getFacetResult(field.getIndexFieldName());
//Check if we are dealing with a date, sometimes the facet values arrive as dates !
if(facetValues.size() == 0 && field.getType().equals(DiscoveryConfigurationParameters.TYPE_DATE)){
facetValues = queryResults.getFacetResult(field.getIndexFieldName() + ".year");
}
int shownFacets = field.getFacetLimit()+1;
//This is needed to make sure that the date filters do not remain empty
if (facetValues != null && 0 < facetValues.size()) {
if(browse == null){
//Since we have a value it is safe to add the sidebar (doing it this way will ensure that we do not end up with an empty sidebar)
browse = options.addList("discovery");
browse.setHead(T_FILTER_HEAD);
}
Iterator<DiscoverResult.FacetResult> iter = facetValues.iterator();
List filterValsList = browse.addList(field.getIndexFieldName());
filterValsList.setHead(message("xmlui.ArtifactBrowser.AdvancedSearch.type_" + field.getIndexFieldName()));
for (int i = 0; i < shownFacets; i++) {
if (!iter.hasNext())
{
//When we have an hierarchical facet always show the "view more" they may want to filter the children of the top nodes
if(field.getType().equals(DiscoveryConfigurationParameters.TYPE_HIERARCHICAL)){
addViewMoreUrl(filterValsList, dso, request, field.getIndexFieldName());
}
break;
}
DiscoverResult.FacetResult value = iter.next();
if (i < shownFacets - 1) {
String displayedValue = value.getDisplayedValue();
String filterQuery = value.getAsFilterQuery();
String filterType = value.getFilterType();
if (fqs.contains(getSearchService().toFilterQuery(context, field.getIndexFieldName(), value.getFilterType(), value.getAsFilterQuery()).getFilterQuery())) {
filterValsList.addItem(Math.random() + "", "selected").addContent(displayedValue + " (" + value.getCount() + ")");
} else {
String paramsQuery = retrieveParameters(request);
filterValsList.addItem().addXref(
contextPath +
(dso == null ? "" : "/handle/" + dso.getHandle()) +
"/discover?" +
paramsQuery +
"filtertype=" + field.getIndexFieldName() +
"&filter_relational_operator="+ filterType +
"&filter=" + encodeForURL(filterQuery),
displayedValue + " (" + value.getCount() + ")"
);
}
}
//Show a "view more" url should there be more values, unless we have a date
if (i == shownFacets - 1 && !field.getType().equals(DiscoveryConfigurationParameters.TYPE_DATE)/*&& facetField.getGap() == null*/) {
addViewMoreUrl(filterValsList, dso, request, field.getIndexFieldName());
}
}
}
}
}
}
}
/**
* Returns the parameters used so it can be used in a url
* @param request the cocoon request
* @return the parameters used on this page
*/
private String retrieveParameters(Request request) throws UnsupportedEncodingException, UIException {
java.util.List<String> parameters = new ArrayList<String>();
if(StringUtils.isNotBlank(request.getParameter("query"))){
parameters.add("query=" + encodeForURL(request.getParameter("query")));
}
if(StringUtils.isNotBlank(request.getParameter("scope"))){
parameters.add("scope=" + request.getParameter("scope"));
}
if(StringUtils.isNotBlank(request.getParameter("sort_by"))){
parameters.add("sort_by=" + request.getParameter("sort_by"));
}
if(StringUtils.isNotBlank(request.getParameter("order"))){
parameters.add("order=" + request.getParameter("order"));
}
if(StringUtils.isNotBlank(request.getParameter("rpp"))){
parameters.add("rpp=" + request.getParameter("rpp"));
}
Map<String, String[]> parameterFilterQueries = DiscoveryUIUtils.getParameterFilterQueries(request);
for(String parameter : parameterFilterQueries.keySet()){
for (int i = 0; i < parameterFilterQueries.get(parameter).length; i++) {
String value = parameterFilterQueries.get(parameter)[i];
parameters.add(parameter + "=" + encodeForURL(value));
}
}
//Join all our parameters using an "&" sign
String parametersString = StringUtils.join(parameters.toArray(new String[parameters.size()]), "&");
if(StringUtils.isNotEmpty(parametersString)){
parametersString += "&";
}
return parametersString;
}
private void addViewMoreUrl(List facet, DSpaceObject dso, Request request, String fieldName) throws WingException, UnsupportedEncodingException {
String parameters = retrieveParameters(request);
facet.addItem().addXref(
contextPath +
(dso == null ? "" : "/handle/" + dso.getHandle()) +
"/search-filter?" + parameters + BrowseFacet.FACET_FIELD + "=" + fieldName,
T_VIEW_MORE
);
}
public DiscoverQuery getQueryArgs(Context context, DSpaceObject scope, String... filterQueries) {
DiscoverQuery queryArgs = new DiscoverQuery();
DiscoveryConfiguration discoveryConfiguration = SearchUtils.getDiscoveryConfiguration(scope);
java.util.List<DiscoverySearchFilterFacet> facets = discoveryConfiguration.getSidebarFacets();
log.info("facets for scope, " + scope + ": " + (facets != null ? facets.size() : null));
if (facets != null){
queryArgs.setFacetMinCount(1);
}
//Add the default filters
queryArgs.addFilterQueries(discoveryConfiguration.getDefaultFilterQueries().toArray(new String[discoveryConfiguration.getDefaultFilterQueries().size()]));
queryArgs.addFilterQueries(filterQueries);
/** enable faceting of search results */
if (facets != null){
for (DiscoverySearchFilterFacet facet : facets) {
if(facet.getType().equals(DiscoveryConfigurationParameters.TYPE_DATE)){
String dateFacet = facet.getIndexFieldName() + ".year";
try{
//Get a range query so we can create facet queries ranging from our first to our last date
//Attempt to determine our oldest & newest year by checking for previously selected filters
int oldestYear = -1;
int newestYear = -1;
for (String filterQuery : filterQueries) {
if(filterQuery.startsWith(dateFacet + ":")){
//Check for a range
Pattern pattern = Pattern.compile("\\[(.*? TO .*?)\\]");
Matcher matcher = pattern.matcher(filterQuery);
boolean hasPattern = matcher.find();
if(hasPattern){
filterQuery = matcher.group(0);
//We have a range
//Resolve our range to a first & last year
int tempOldYear = Integer.parseInt(filterQuery.split(" TO ")[0].replace("[", "").trim());
int tempNewYear = Integer.parseInt(filterQuery.split(" TO ")[1].replace("]", "").trim());
//Check if we have a further filter (or a first one found)
if(tempNewYear < newestYear || oldestYear < tempOldYear || newestYear == -1){
oldestYear = tempOldYear;
newestYear = tempNewYear;
}
}else{
if(filterQuery.indexOf(" OR ") != -1){
//Should always be the case
filterQuery = filterQuery.split(" OR ")[0];
}
//We should have a single date
oldestYear = Integer.parseInt(filterQuery.split(":")[1].trim());
newestYear = oldestYear;
//No need to look further
break;
}
}
}
//Check if we have found a range, if not then retrieve our first & last year using Solr
if(oldestYear == -1 && newestYear == -1){
DiscoverQuery yearRangeQuery = new DiscoverQuery();
yearRangeQuery.setMaxResults(1);
//Set our query to anything that has this value
yearRangeQuery.addFieldPresentQueries(dateFacet);
//Set sorting so our last value will appear on top
yearRangeQuery.setSortField(dateFacet + "_sort", DiscoverQuery.SORT_ORDER.asc);
yearRangeQuery.addFilterQueries(filterQueries);
yearRangeQuery.addSearchField(dateFacet);
DiscoverResult lastYearResult = getSearchService().search(context, scope, yearRangeQuery);
if(0 < lastYearResult.getDspaceObjects().size()){
java.util.List<DiscoverResult.SearchDocument> searchDocuments = lastYearResult.getSearchDocument(lastYearResult.getDspaceObjects().get(0));
if(0 < searchDocuments.size() && 0 < searchDocuments.get(0).getSearchFieldValues(dateFacet).size()){
oldestYear = Integer.parseInt(searchDocuments.get(0).getSearchFieldValues(dateFacet).get(0));
}
}
//Now get the first year
yearRangeQuery.setSortField(dateFacet + "_sort", DiscoverQuery.SORT_ORDER.desc);
DiscoverResult firstYearResult = getSearchService().search(context, scope, yearRangeQuery);
if( 0 < firstYearResult.getDspaceObjects().size()){
java.util.List<DiscoverResult.SearchDocument> searchDocuments = firstYearResult.getSearchDocument(firstYearResult.getDspaceObjects().get(0));
if(0 < searchDocuments.size() && 0 < searchDocuments.get(0).getSearchFieldValues(dateFacet).size()){
newestYear = Integer.parseInt(searchDocuments.get(0).getSearchFieldValues(dateFacet).get(0));
}
}
//No values found!
if(newestYear == -1 || oldestYear == -1)
{
continue;
}
}
int gap = 1;
//Attempt to retrieve our gap using the algorithm below
int yearDifference = newestYear - oldestYear;
if(yearDifference != 0){
while (10 < ((double)yearDifference / gap)){
gap *= 10;
}
}
// We need to determine our top year so we can start our count from a clean year
// Example: 2001 and a gap from 10 we need the following result: 2010 - 2000 ; 2000 - 1990 hence the top year
int topYear = (int) (Math.ceil((float) (newestYear)/gap)*gap);
if(gap == 1){
//We need a list of our years
//We have a date range add faceting for our field
//The faceting will automatically be limited to the 10 years in our span due to our filterquery
queryArgs.addFacetField(new DiscoverFacetField(facet.getIndexFieldName(), facet.getType(), 10, facet.getSortOrder()));
}else{
java.util.List<String> facetQueries = new ArrayList<String>();
//Create facet queries but limit them to 11 (11 == when we need to show a "show more" url)
for(int year = topYear; year > oldestYear && (facetQueries.size() < 11); year-=gap){
//Add a filter to remove the last year only if we aren't the last year
int bottomYear = year - gap;
//Make sure we don't go below our last year found
if(bottomYear < oldestYear)
{
bottomYear = oldestYear;
}
//Also make sure we don't go above our newest year
int currentTop = year;
if((year == topYear))
{
currentTop = newestYear;
}
else
{
//We need to do -1 on this one to get a better result
currentTop--;
}
facetQueries.add(dateFacet + ":[" + bottomYear + " TO " + currentTop + "]");
}
for (String facetQuery : facetQueries) {
queryArgs.addFacetQuery(facetQuery);
}
}
}catch (Exception e){
log.error(LogManager.getHeader(context, "Error in Discovery while setting up date facet range", "date facet: " + dateFacet), e);
}
}else{
int facetLimit = facet.getFacetLimit();
//Add one to our facet limit to make sure that if we have more then the shown facets that we show our "show more" url
facetLimit++;
queryArgs.addFacetField(new DiscoverFacetField(facet.getIndexFieldName(), facet.getType(), facetLimit, facet.getSortOrder()));
}
}
}
return queryArgs;
}
/**
* Determine the current scope. This may be derived from the current url
* handle if present or the scope parameter is given. If no scope is
* specified then null is returned.
*
* @return The current scope.
*/
private DSpaceObject getScope() throws SQLException {
Request request = ObjectModelHelper.getRequest(objectModel);
String scopeString = request.getParameter("scope");
// Are we in a community or collection?
DSpaceObject dso;
if (scopeString == null || "".equals(scopeString))
{
// get the search scope from the url handle
dso = HandleUtil.obtainHandle(objectModel);
}
else
{
// Get the search scope from the location parameter
dso = HandleManager.resolveToObject(context, scopeString);
}
return dso;
}
@Override
public void recycle() {
queryResults = null;
queryArgs = null;
validity = null;
super.recycle();
}
}