Redesign - much better and working correctly

This commit is contained in:
William Welling
2014-12-13 21:05:59 -06:00
parent 2d211e500b
commit 34011181f8
4 changed files with 182 additions and 124 deletions

View File

@@ -23,6 +23,7 @@ import org.dspace.app.xmlui.cocoon.AbstractDSpaceTransformer;
import org.dspace.app.xmlui.utils.DSpaceValidity; import org.dspace.app.xmlui.utils.DSpaceValidity;
import org.dspace.app.xmlui.utils.HandleUtil; import org.dspace.app.xmlui.utils.HandleUtil;
import org.dspace.app.xmlui.utils.UIException; import org.dspace.app.xmlui.utils.UIException;
import org.dspace.app.xmlui.utils.ContextUtil;
import org.dspace.app.xmlui.wing.Message; import org.dspace.app.xmlui.wing.Message;
import org.dspace.app.xmlui.wing.WingException; import org.dspace.app.xmlui.wing.WingException;
import org.dspace.app.xmlui.wing.element.*; import org.dspace.app.xmlui.wing.element.*;
@@ -91,28 +92,7 @@ public abstract class AbstractSearch extends AbstractDSpaceTransformer implement
* Cached query results * Cached query results
*/ */
protected DiscoverResult queryResults; protected DiscoverResult queryResults;
/**
* Static query results for exporting metadata
*/
private static DiscoverResult staticQueryResults;
public static boolean isStaticQueryResults = false;
public void setStaticQueryResults(DiscoverResult qResults) {
staticQueryResults = qResults;
isStaticQueryResults = true;
}
public static DiscoverResult getStaticQueryResults() {
return staticQueryResults;
}
public static void freeStaticQueryResults() {
staticQueryResults = null;
isStaticQueryResults = false;
}
/** /**
* Cached query arguments * Cached query arguments
*/ */
@@ -736,28 +716,24 @@ public abstract class AbstractSearch extends AbstractDSpaceTransformer implement
* *
* @param scope the dspace object parent * @param scope the dspace object parent
*/ */
public void performSearch(DSpaceObject scope) throws UIException, SearchServiceException { public void performSearch(DSpaceObject scope) throws UIException, SearchServiceException
{
if (queryResults != null) if (queryResults != null) {
{
return; return;
} }
String query = getQuery(); String query = getQuery();
//DSpaceObject scope = getScope();
int page = getParameterPage(); int page = getParameterPage();
List<String> filterQueries = new ArrayList<String>(); List<String> filterQueries = new ArrayList<String>();
String[] fqs = getFilterQueries(); String[] fqs = getFilterQueries();
if (fqs != null) if (fqs != null) {
{
filterQueries.addAll(Arrays.asList(fqs)); filterQueries.addAll(Arrays.asList(fqs));
} }
this.queryArgs = new DiscoverQuery(); this.queryArgs = new DiscoverQuery();
queryArgs.setMaxResults(getParameterRpp()); queryArgs.setMaxResults(getParameterRpp());
@@ -773,35 +749,32 @@ public abstract class AbstractSearch extends AbstractDSpaceTransformer implement
String sortBy = ObjectModelHelper.getRequest(objectModel).getParameter("sort_by"); String sortBy = ObjectModelHelper.getRequest(objectModel).getParameter("sort_by");
DiscoverySortConfiguration searchSortConfiguration = discoveryConfiguration.getSearchSortConfiguration(); DiscoverySortConfiguration searchSortConfiguration = discoveryConfiguration.getSearchSortConfiguration();
if(sortBy == null){ if(sortBy == null) {
//Attempt to find the default one, if none found we use SCORE //Attempt to find the default one, if none found we use SCORE
sortBy = "score"; sortBy = "score";
if(searchSortConfiguration != null){ if(searchSortConfiguration != null) {
for (DiscoverySortFieldConfiguration sortFieldConfiguration : searchSortConfiguration.getSortFields()) { for (DiscoverySortFieldConfiguration sortFieldConfiguration : searchSortConfiguration.getSortFields()) {
if(sortFieldConfiguration.equals(searchSortConfiguration.getDefaultSort())){ if(sortFieldConfiguration.equals(searchSortConfiguration.getDefaultSort())) {
sortBy = SearchUtils.getSearchService().toSortFieldIndex(sortFieldConfiguration.getMetadataField(), sortFieldConfiguration.getType()); sortBy = SearchUtils.getSearchService().toSortFieldIndex(sortFieldConfiguration.getMetadataField(), sortFieldConfiguration.getType());
} }
} }
} }
} }
String sortOrder = ObjectModelHelper.getRequest(objectModel).getParameter("order"); String sortOrder = ObjectModelHelper.getRequest(objectModel).getParameter("order");
if(sortOrder == null && searchSortConfiguration != null){ if(sortOrder == null && searchSortConfiguration != null) {
sortOrder = searchSortConfiguration.getDefaultSortOrder().toString(); sortOrder = searchSortConfiguration.getDefaultSortOrder().toString();
} }
if (sortOrder == null || sortOrder.equalsIgnoreCase("DESC")) if (sortOrder == null || sortOrder.equalsIgnoreCase("DESC")) {
{
queryArgs.setSortField(sortBy, DiscoverQuery.SORT_ORDER.desc); queryArgs.setSortField(sortBy, DiscoverQuery.SORT_ORDER.desc);
} }
else else {
{
queryArgs.setSortField(sortBy, DiscoverQuery.SORT_ORDER.asc); queryArgs.setSortField(sortBy, DiscoverQuery.SORT_ORDER.asc);
} }
String groupBy = ObjectModelHelper.getRequest(objectModel).getParameter("group_by"); String groupBy = ObjectModelHelper.getRequest(objectModel).getParameter("group_by");
// Enable groupBy collapsing if designated // Enable groupBy collapsing if designated
if (groupBy != null && !groupBy.equalsIgnoreCase("none")) { if (groupBy != null && !groupBy.equalsIgnoreCase("none")) {
/** Construct a Collapse Field Query */ /** Construct a Collapse Field Query */
@@ -816,25 +789,20 @@ public abstract class AbstractSearch extends AbstractDSpaceTransformer implement
// TODO: I think that can be more transparently done in the solr solrconfig.xml with DISMAX and boosting // TODO: I think that can be more transparently done in the solr solrconfig.xml with DISMAX and boosting
/** sort in groups to get publications to top */ /** sort in groups to get publications to top */
queryArgs.setSortField("dc.type", DiscoverQuery.SORT_ORDER.asc); queryArgs.setSortField("dc.type", DiscoverQuery.SORT_ORDER.asc);
} }
queryArgs.setQuery(query != null && !query.trim().equals("") ? query : null); queryArgs.setQuery(query != null && !query.trim().equals("") ? query : null);
if (page > 1) if (page > 1) {
{
queryArgs.setStart((page - 1) * queryArgs.getMaxResults()); queryArgs.setStart((page - 1) * queryArgs.getMaxResults());
} }
else else {
{
queryArgs.setStart(0); queryArgs.setStart(0);
} }
if(discoveryConfiguration.getHitHighlightingConfiguration() != null) if(discoveryConfiguration.getHitHighlightingConfiguration() != null) {
{
List<DiscoveryHitHighlightFieldConfiguration> metadataFields = discoveryConfiguration.getHitHighlightingConfiguration().getMetadataFields(); List<DiscoveryHitHighlightFieldConfiguration> metadataFields = discoveryConfiguration.getHitHighlightingConfiguration().getMetadataFields();
for (DiscoveryHitHighlightFieldConfiguration fieldConfiguration : metadataFields) for (DiscoveryHitHighlightFieldConfiguration fieldConfiguration : metadataFields) {
{
queryArgs.addHitHighlightingField(new DiscoverHitHighlightingField(fieldConfiguration.getField(), fieldConfiguration.getMaxSize(), fieldConfiguration.getSnippets())); queryArgs.addHitHighlightingField(new DiscoverHitHighlightingField(fieldConfiguration.getField(), fieldConfiguration.getMaxSize(), fieldConfiguration.getSnippets()));
} }
} }
@@ -842,18 +810,6 @@ public abstract class AbstractSearch extends AbstractDSpaceTransformer implement
queryArgs.setSpellCheck(discoveryConfiguration.isSpellCheckEnabled()); queryArgs.setSpellCheck(discoveryConfiguration.isSpellCheckEnabled());
this.queryResults = SearchUtils.getSearchService().search(context, scope, queryArgs); this.queryResults = SearchUtils.getSearchService().search(context, scope, queryArgs);
if(page == 1) {
queryArgs.setMaxResults(safeLongToInt(this.queryResults.getTotalSearchResults()));
setStaticQueryResults(SearchUtils.getSearchService().search(context, scope, queryArgs));
}
}
public static int safeLongToInt(long l) {
if (l < Integer.MIN_VALUE || l > Integer.MAX_VALUE) {
throw new IllegalArgumentException(l + " cannot be cast to int.");
}
return (int) l;
} }
/** /**
@@ -861,35 +817,135 @@ public abstract class AbstractSearch extends AbstractDSpaceTransformer implement
* *
* @throws IOException * @throws IOException
*/ */
public static DSpaceCSV exportMetadata(Context context) throws IOException public DSpaceCSV exportMetadata(Map objectModel, String query, String filters) throws IOException, UIException, SearchServiceException, SQLException
{ {
DiscoverResult qResults = new DiscoverResult();
DiscoverQuery qArgs = new DiscoverQuery();
Context context = ContextUtil.obtainContext(objectModel);
Request request = ObjectModelHelper.getRequest(objectModel);
DSpaceObject scope = HandleUtil.obtainHandle(objectModel);
List<String> filterQueries = new ArrayList<String>();
String[] fqs = filters.split(",");
if (fqs != null) {
filterQueries.addAll(Arrays.asList(fqs));
}
qArgs.setMaxResults(getParameterRpp());
//Add the configured default filter queries
DiscoveryConfiguration discoveryConfiguration = SearchUtils.getDiscoveryConfiguration(scope);
List<String> defaultFilterQueries = discoveryConfiguration.getDefaultFilterQueries();
qArgs.addFilterQueries(defaultFilterQueries.toArray(new String[defaultFilterQueries.size()]));
if (filterQueries.size() > 0) {
qArgs.addFilterQueries(filterQueries.toArray(new String[filterQueries.size()]));
}
String sortBy = ObjectModelHelper.getRequest(objectModel).getParameter("sort_by");
DiscoverySortConfiguration searchSortConfiguration = discoveryConfiguration.getSearchSortConfiguration();
if(sortBy == null) {
//Attempt to find the default one, if none found we use SCORE
sortBy = "score";
if(searchSortConfiguration != null) {
for (DiscoverySortFieldConfiguration sortFieldConfiguration : searchSortConfiguration.getSortFields()) {
if(sortFieldConfiguration.equals(searchSortConfiguration.getDefaultSort())) {
sortBy = SearchUtils.getSearchService().toSortFieldIndex(sortFieldConfiguration.getMetadataField(), sortFieldConfiguration.getType());
}
}
}
}
String sortOrder = ObjectModelHelper.getRequest(objectModel).getParameter("order");
if(sortOrder == null && searchSortConfiguration != null) {
sortOrder = searchSortConfiguration.getDefaultSortOrder().toString();
}
if (sortOrder == null || sortOrder.equalsIgnoreCase("DESC")) {
qArgs.setSortField(sortBy, DiscoverQuery.SORT_ORDER.desc);
}
else {
qArgs.setSortField(sortBy, DiscoverQuery.SORT_ORDER.asc);
}
String groupBy = ObjectModelHelper.getRequest(objectModel).getParameter("group_by");
// Enable groupBy collapsing if designated
if (groupBy != null && !groupBy.equalsIgnoreCase("none")) {
/** Construct a Collapse Field Query */
qArgs.addProperty("collapse.field", groupBy);
qArgs.addProperty("collapse.threshold", "1");
qArgs.addProperty("collapse.includeCollapsedDocs.fl", "handle");
qArgs.addProperty("collapse.facet", "before");
//queryArgs.a type:Article^2
// TODO: This is a hack to get Publications (Articles) to always be at the top of Groups.
// TODO: I think that can be more transparently done in the solr solrconfig.xml with DISMAX and boosting
/** sort in groups to get publications to top */
qArgs.setSortField("dc.type", DiscoverQuery.SORT_ORDER.asc);
}
qArgs.setQuery(query != null && !query.trim().equals("") ? query : null);
qArgs.setStart(0);
if(discoveryConfiguration.getHitHighlightingConfiguration() != null) {
List<DiscoveryHitHighlightFieldConfiguration> metadataFields = discoveryConfiguration.getHitHighlightingConfiguration().getMetadataFields();
for (DiscoveryHitHighlightFieldConfiguration fieldConfiguration : metadataFields) {
qArgs.addHitHighlightingField(new DiscoverHitHighlightingField(fieldConfiguration.getField(), fieldConfiguration.getMaxSize(), fieldConfiguration.getSnippets()));
}
}
qArgs.setSpellCheck(discoveryConfiguration.isSpellCheckEnabled());
qResults = SearchUtils.getSearchService().search(context, scope, qArgs);
qArgs.setMaxResults(safeLongToInt(qResults.getTotalSearchResults()));
qResults = SearchUtils.getSearchService().search(context, scope, qArgs);
Item[] resultsItems; Item[] resultsItems;
// Get a list of found items
// Get a list of found items
ArrayList<Item> items = new ArrayList<Item>(); ArrayList<Item> items = new ArrayList<Item>();
for (DSpaceObject resultDSO : getStaticQueryResults().getDspaceObjects()) for (DSpaceObject resultDSO : qResults.getDspaceObjects()) {
{ if (resultDSO instanceof Item) {
if (resultDSO instanceof Item)
{
items.add((Item) resultDSO); items.add((Item) resultDSO);
} }
} }
resultsItems = new Item[items.size()]; resultsItems = new Item[items.size()];
resultsItems = items.toArray(resultsItems); resultsItems = items.toArray(resultsItems);
// Log the attempt // Log the attempt
log.info(LogManager.getHeader(context, "metadataexport", "exporting_search")); log.info(LogManager.getHeader(context, "metadataexport", "exporting_search"));
// Export a search view // Export a search view
ArrayList iids = new ArrayList(); ArrayList iids = new ArrayList();
for (Item item : items) for (Item item : items) {
{
iids.add(item.getID()); iids.add(item.getID());
} }
ItemIterator ii = new ItemIterator(context, iids); ItemIterator ii = new ItemIterator(context, iids);
MetadataExport exporter = new MetadataExport(context, ii, false); MetadataExport exporter = new MetadataExport(context, ii, false);
// Perform the export // Perform the export
DSpaceCSV csv = exporter.export(); DSpaceCSV csv = exporter.export();
log.info(LogManager.getHeader(context, "metadataexport", "exported_file:search-results.csv")); log.info(LogManager.getHeader(context, "metadataexport", "exported_file:search-results.csv"));
return csv; return csv;
} }
public static int safeLongToInt(long l) {
if (l < Integer.MIN_VALUE || l > Integer.MAX_VALUE) {
throw new IllegalArgumentException(l + " cannot be cast to int.");
}
return (int) l;
}
/** /**
* Returns a list of the filter queries for use in rendering pages, creating page more urls, .... * Returns a list of the filter queries for use in rendering pages, creating page more urls, ....
@@ -1128,5 +1184,4 @@ public abstract class AbstractSearch extends AbstractDSpaceTransformer implement
+ (queryArgs == null ? "" : queryArgs.getQuery()) + "\",results=(" + countCommunities + "," + (queryArgs == null ? "" : queryArgs.getQuery()) + "\",results=(" + countCommunities + ","
+ countCollections + "," + countItems + ")")); + countCollections + "," + countItems + ")"));
} }
} }

View File

@@ -34,6 +34,7 @@ import org.dspace.content.Item;
import org.dspace.content.Collection; import org.dspace.content.Collection;
import org.dspace.content.Community; import org.dspace.content.Community;
import org.dspace.content.DSpaceObject; import org.dspace.content.DSpaceObject;
import org.dspace.discovery.*;
import org.xml.sax.SAXException; import org.xml.sax.SAXException;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
@@ -57,23 +58,7 @@ public class Navigation extends AbstractDSpaceTransformer implements CacheablePr
* This key must be unique inside the space of this component. * This key must be unique inside the space of this component.
*/ */
public Serializable getKey() { public Serializable getKey() {
try { return "0";
Request request = ObjectModelHelper.getRequest(objectModel);
String key = request.getScheme() + request.getServerName() + request.getServerPort() + request.getSitemapURI() + request.getQueryString();
DSpaceObject dso = HandleUtil.obtainHandle(objectModel);
if (dso != null)
{
key += "-" + dso.getHandle();
}
return HashUtil.hash(key);
}
catch (SQLException sqle)
{
// Ignore all errors and just return that the component is not cachable.
return "0";
}
} }
/** /**
@@ -143,28 +128,37 @@ public class Navigation extends AbstractDSpaceTransformer implements CacheablePr
String search_export_config = ConfigurationManager.getProperty("xmlui.search.metadata_export"); String search_export_config = ConfigurationManager.getProperty("xmlui.search.metadata_export");
if(uri.contains("discover")) { String query = decodeFromURL(request.getParameter("query"));
String fqps = "";
String[] fqs = DiscoveryUIUtils.getFilterQueries(ObjectModelHelper.getRequest(objectModel), context);
if (fqs != null)
{
for(int i = 0; i < fqs.length; i++) {
if(i < fqs.length - 1)
fqps += fqs[i] + ",";
else
fqps += fqs[i];
}
}
if(uri.contains("discover")) {
if(search_export_config != null) { if(search_export_config != null) {
if(search_export_config.equals("admin")) { if(search_export_config.equals("admin")) {
if(AuthorizeManager.isAdmin(context)) { if(AuthorizeManager.isAdmin(context)) {
List results = options.addList("context"); List results = options.addList("context");
results.setHead(T_context_head); results.setHead(T_context_head);
results.addItem().addXref(contextPath + "/discover/csv", T_export_metadata); results.addItem().addXref(contextPath + "/discover/csv/" + query + "/" + fqps, T_export_metadata);
} }
} }
else if(search_export_config.equals("user") || search_export_config.equals("anonymous")){ else if(search_export_config.equals("user") || search_export_config.equals("anonymous")){
List results = options.addList("context"); List results = options.addList("context");
results.setHead(T_context_head); results.setHead(T_context_head);
results.addItem().addXref(contextPath + "/discover/csv", T_export_metadata); results.addItem().addXref(contextPath + "/discover/csv/" + query + "/" + fqps, T_export_metadata);
} }
} }
} }
else {
if(AbstractSearch.isStaticQueryResults) {
AbstractSearch.freeStaticQueryResults();
}
}
} }
/** /**
@@ -179,7 +173,4 @@ public class Navigation extends AbstractDSpaceTransformer implements CacheablePr
pageMeta.addMetadata("search", "advancedURL").addContent(contextPath + "/discover"); pageMeta.addMetadata("search", "advancedURL").addContent(contextPath + "/discover");
pageMeta.addMetadata("search", "queryField").addContent("query"); pageMeta.addMetadata("search", "queryField").addContent("query");
} }
}
}

View File

@@ -35,6 +35,7 @@ import org.dspace.app.xmlui.wing.element.Body;
import org.dspace.app.xmlui.utils.UIException; import org.dspace.app.xmlui.utils.UIException;
import org.dspace.app.xmlui.utils.AuthenticationUtil; import org.dspace.app.xmlui.utils.AuthenticationUtil;
import org.dspace.app.xmlui.utils.ContextUtil; import org.dspace.app.xmlui.utils.ContextUtil;
import org.dspace.app.xmlui.aspect.discovery.AbstractSearch; import org.dspace.app.xmlui.aspect.discovery.AbstractSearch;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
@@ -52,7 +53,6 @@ import org.dspace.content.DSpaceObject;
import org.dspace.content.ItemIterator; import org.dspace.content.ItemIterator;
/** /**
* *
* AbstractReader that generates a CSV of search * AbstractReader that generates a CSV of search
@@ -62,6 +62,8 @@ import org.dspace.content.ItemIterator;
public class SearchMetadataExportReader extends AbstractReader implements Recyclable public class SearchMetadataExportReader extends AbstractReader implements Recyclable
{ {
private static Logger log = Logger.getLogger(MetadataExportReader.class);
/** /**
* Messages to be sent when the user is not authorized to view * Messages to be sent when the user is not authorized to view
* a particular bitstream. They will be redirected to the login * a particular bitstream. They will be redirected to the login
@@ -91,8 +93,6 @@ public class SearchMetadataExportReader extends AbstractReader implements Recycl
/** The Cocoon request */ /** The Cocoon request */
protected Request request; protected Request request;
private static Logger log = Logger.getLogger(MetadataExportReader.class);
DSpaceCSV csv = null; DSpaceCSV csv = null;
String filename = null; String filename = null;
@@ -101,24 +101,27 @@ public class SearchMetadataExportReader extends AbstractReader implements Recycl
* *
* See the class description for information on configuration options. * See the class description for information on configuration options.
*/ */
public void setup(SourceResolver resolver, Map objectModel, String src, public void setup(SourceResolver resolver, Map objectModel, String src, Parameters par) throws ProcessingException, SAXException, IOException
Parameters par) throws ProcessingException, SAXException,
IOException
{ {
super.setup(resolver, objectModel, src, par); super.setup(resolver, objectModel, src, par);
try try
{ {
this.request = ObjectModelHelper.getRequest(objectModel); this.request = ObjectModelHelper.getRequest(objectModel);
this.response = ObjectModelHelper.getResponse(objectModel); this.response = ObjectModelHelper.getResponse(objectModel);
String query = par.getParameter("query");
String filters = par.getParameter("filters");
ExportSearch exportSearch = new ExportSearch();
Context context = ContextUtil.obtainContext(objectModel); Context context = ContextUtil.obtainContext(objectModel);
String search_export_config = ConfigurationManager.getProperty("xmlui.search.metadata_export"); String search_export_config = ConfigurationManager.getProperty("xmlui.search.metadata_export");
if(search_export_config.equals("admin")) { if(search_export_config.equals("admin")) {
if(AuthorizeManager.isAdmin(context)) { if(AuthorizeManager.isAdmin(context)) {
csv = AbstractSearch.exportMetadata(context); csv = exportSearch.exportMetadata(objectModel, query, filters);
filename = "search-results.csv"; filename = "search-results.csv";
} }
else { else {
@@ -144,7 +147,7 @@ public class SearchMetadataExportReader extends AbstractReader implements Recycl
} }
else if(search_export_config.equals("user")) { else if(search_export_config.equals("user")) {
if(AuthenticationUtil.isLoggedIn(request)) { if(AuthenticationUtil.isLoggedIn(request)) {
csv = AbstractSearch.exportMetadata(context); csv = exportSearch.exportMetadata(objectModel, query, filters);
filename = "search-results.csv"; filename = "search-results.csv";
} }
else { else {
@@ -156,20 +159,17 @@ public class SearchMetadataExportReader extends AbstractReader implements Recycl
} }
} }
else if(search_export_config.equals("anonymous")) { else if(search_export_config.equals("anonymous")) {
csv = AbstractSearch.exportMetadata(context); csv = exportSearch.exportMetadata(objectModel, query, filters);
filename = "search-results.csv"; filename = "search-results.csv";
} }
} }
catch (RuntimeException e) catch (RuntimeException e) {
{
throw e; throw e;
} }
catch (IOException e) catch (IOException e) {
{
throw new ProcessingException("Unable to export metadata.",e); throw new ProcessingException("Unable to export metadata.",e);
} }
catch (Exception e) catch (Exception e) {
{
throw new ProcessingException("Unable to read bitstream.",e); throw new ProcessingException("Unable to read bitstream.",e);
} }
} }
@@ -192,5 +192,14 @@ public class SearchMetadataExportReader extends AbstractReader implements Recycl
public void recycle() { public void recycle() {
this.response = null; this.response = null;
this.request = null; this.request = null;
} }
}
class ExportSearch extends AbstractSearch
{
public ExportSearch() {}
protected String getQuery() throws UIException { return null; }
protected String getBasicUrl() throws SQLException { return null; }
protected String generateURL(Map<String, String> parameters) throws UIException { return null; }
public void addBody(Body body) throws SAXException, WingException, UIException, SQLException, IOException, AuthorizeException {}
} }

View File

@@ -391,8 +391,11 @@
</map:read> </map:read>
</map:match> </map:match>
<map:match pattern="discover/csv"> <map:match pattern="discover/csv/*/*">
<map:read type="SearchMetadataExportReader"/> <map:read type="SearchMetadataExportReader">
<map:parameter name="query" value="{1}"/>
<map:parameter name="filters" value="{2}"/>
</map:read>
</map:match> </map:match>
<map:match pattern="handle/*/*/stats/csv"> <map:match pattern="handle/*/*/stats/csv">