com.day.cq.wcm.foundation.Search Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of aem-sdk-api Show documentation
Show all versions of aem-sdk-api Show documentation
The Adobe Experience Manager SDK
The newest version!
/*
* Copyright 1997-2008 Day Management AG
* Barfuesserplatz 6, 4001 Basel, Switzerland
* All Rights Reserved.
*
* This software is the confidential and proprietary information of
* Day Management AG, ("Confidential Information"). You shall not
* disclose such Confidential Information and shall use it only in
* accordance with the terms of the license agreement you entered into
* with Day.
*/
package com.day.cq.wcm.foundation;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.jcr.query.Query;
import javax.jcr.query.RowIterator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.commons.lang.StringUtils;
import org.apache.jackrabbit.commons.query.GQL;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.request.RequestParameter;
import org.apache.sling.api.resource.Resource;
import com.day.cq.commons.jcr.JcrConstants;
import com.day.cq.dam.api.DamConstants;
import com.day.cq.search.Predicate;
import com.day.cq.search.PredicateGroup;
import com.day.cq.search.SimpleSearch;
import com.day.cq.search.Trends;
import com.day.cq.search.QueryBuilder;
import com.day.cq.search.facets.Facet;
import com.day.cq.search.result.ResultPage;
import com.day.cq.search.result.SearchResult;
import com.day.cq.wcm.api.NameConstants;
import com.day.cq.wcm.api.components.Component;
import com.day.cq.wcm.commons.WCMUtils;
/**
* The Search
class implements the search logic used in the
* foundation search component and exposes the query result in a scripting
* friendly object structure.
*
* This class does a fulltext query, which means wildcards like '*' and '?' will
* be filtered out. Please use {@link QueryBuilder} or execute a query directly
* on the JCR Session if wildcard support is needed.
*/
public final class Search {
private final Logger log = LoggerFactory.getLogger(Search.class);
/**
* The name for the query parameter.
*/
private static final String QUERY_PARAM_NAME = "q";
/**
* The name for the start parameter.
*/
private static final String START_PARAM_NAME = "start";
/**
* The name for the language facet parameter.
*/
private static final String LANGUAGE_FACET_PARAM_NAME = "language";
/**
* The name for the charset parameter (usually set when posting).
*/
private static final String CHARSET_PARAM_NAME = "_charset_";
/**
* The name for the tag facet parameter.
*/
private static final String TAG_FACET_PARAM_NAME = "tag";
/**
* The name for the mime type facet parameter.
*/
private static final String MIME_TYPE_FACET_PARAM_NAME = "mimeType";
/**
* The name for the last modified facet parameter, which specifies the
* lower bound of the date range.
*/
private static final String FROM_FACET_PARAM_NAME = "from";
/**
* The name for the last modified facet parameter, which specifies the
* upper bound of the date range.
*/
private static final String TO_FACET_PARAM_NAME = "to";
/**
* The name for the node types parameter
*/
private static final String NODE_TYPE_PARAM_NAME = "nodeType";
/**
* HTML extension.
*/
private static final String HTML_EXT = "html";
/**
* Regular expression that matches special characters
*/
private static final String SPECIAL_CHARS_REGEX = "[!@#$%^&*()?_,;'\"|<>`~±§{}\\-\\\\\\/\\.\\s\\n\\r]*";
/**
* The query template to check the spelling of a fulltext query statement.
*/
private static final String SPELLCHECK_QUERY = "SELECT [rep:spellcheck()] FROM [${nodeType}] AS a WHERE [jcr:path] = '/' AND SPELLCHECK('${query}')";
/**
* Default node types that will be searched if the node type request parameter is empty
*/
private List defaultNodeTypes = new ArrayList(Arrays.asList("cq:Page", "dam:Asset"));
/**
* The current request.
*/
private final SlingHttpServletRequest request;
/**
* The underlying search instance.
*/
private final SimpleSearch search;
/**
* The current resource.
*/
private final Resource resource;
/**
* The query result. Initially null
.
*/
private Result result;
/**
* The result pages.
*/
private List resultPages;
/**
* The raw query as read from the request or manually set.
*/
private String query;
/**
* Set to true
if there was a tag predicate set in the request.
*/
private boolean tagPredicateSet;
/**
* Creates a new search based on the given request
.
*
* @param request the current request.
*/
public Search(SlingHttpServletRequest request) {
this.request = request;
this.resource = request.getResource();
this.search = resource.adaptTo(SimpleSearch.class);
String charset = "ISO-8859-1"; //would be used by the GET requests
if (request.getParameter(CHARSET_PARAM_NAME) != null) {
charset = request.getParameter(CHARSET_PARAM_NAME);
}
if (request.getParameter(QUERY_PARAM_NAME) != null) {
try {
setQuery(new String(request.getParameter(
QUERY_PARAM_NAME).getBytes(charset), "UTF-8"));
} catch (UnsupportedEncodingException e) {
// ignore
}
}
if (request.getParameter(START_PARAM_NAME) != null) {
try {
this.search.setStart(Long.parseLong(request.getParameter(START_PARAM_NAME)));
} catch (NumberFormatException e) {
// ignore
}
}
// Note: the predicate names (first param to Predicate constructor) are the same
// as in the facets returned and used in search.jsp
Predicate languagePredicate = new Predicate("languages", "language");
languagePredicate.set("language", request.getParameter(LANGUAGE_FACET_PARAM_NAME));
this.search.addPredicate(languagePredicate);
Predicate tagPredicate = new Predicate("tags", "tagid");
tagPredicate.set("property", JcrConstants.JCR_CONTENT + "/" + NameConstants.PN_TAGS);
tagPredicate.set("tagid", request.getParameter(TAG_FACET_PARAM_NAME));
this.search.addPredicate(tagPredicate);
this.tagPredicateSet = tagPredicate.get("tagid") != null;
Predicate mimeTypePredicate = new Predicate("mimeTypes", "property");
mimeTypePredicate.set("property", JcrConstants.JCR_CONTENT + "/" + JcrConstants.JCR_MIMETYPE);
mimeTypePredicate.set("value", request.getParameter(MIME_TYPE_FACET_PARAM_NAME));
this.search.addPredicate(mimeTypePredicate);
Predicate lastModPredicate = new Predicate("lastModified", "daterange");
lastModPredicate.set("property", JcrConstants.JCR_CONTENT + "/" + NameConstants.PN_PAGE_LAST_MOD);
// TODO: consider lowerOperation/upperOperation (has to be additional request params)
lastModPredicate.set("lowerBound", request.getParameter(FROM_FACET_PARAM_NAME));
lastModPredicate.set("upperBound", request.getParameter(TO_FACET_PARAM_NAME));
this.search.addPredicate(lastModPredicate);
Predicate orderByScore = new Predicate("orderByScore", Predicate.ORDER_BY);
orderByScore.set(Predicate.ORDER_BY, "@jcr:score");
orderByScore.set(Predicate.PARAM_SORT, Predicate.SORT_DESCENDING);
this.search.addPredicate(orderByScore);
// add a predicate to filter out asset renditions
Predicate assetRenditionFilterPred = new Predicate("assetRenditionFilter", "assetRenditionFilter");
this.search.addPredicate(assetRenditionFilterPred);
// add a predicate to filter out sub-assets
Predicate subAssetFilterPred = new Predicate("subAssetFilter", "subAssetFilter");
this.search.addPredicate(subAssetFilterPred);
addNodeTypesPredicate(getNodeTypes());
}
/**
* @return query trends (popular queries).
*/
public Trends getTrends() {
return search.getTrends();
}
/**
* @return the query result or null
if there is neither a query
* parameter set nor a tag predicate.
* @throws RepositoryException if an exception occurs while executing the
* query.
*/
public Result getResult() throws RepositoryException {
if (result == null
&& (search.getQuery().length() > 0 || tagPredicateSet)
&& search.getResult() != null) {
result = new Result(search.getResult());
}
return result;
}
/**
* @return queries that are related to the current one.
* @throws RepositoryException if an error occurs while reading from
* the repository.
*/
public List getRelatedQueries() throws RepositoryException {
return search.getRelatedQueries();
}
/**
* @return the query supplied by the user or an empty String if none is
* provided.
*/
public String getQuery() {
return query != null ? query : "";
}
private String getSpellcheckerQuery() {
String spellcheckerQuery = StringUtils.EMPTY;
List nodeTypes = getNodeTypes();
int i = 1;
for(String nodeType: nodeTypes) {
spellcheckerQuery += SPELLCHECK_QUERY
.replaceAll("\\$\\{query\\}", Matcher.quoteReplacement(getQuery()))
.replaceAll("\\$\\{nodeType\\}", Matcher.quoteReplacement(nodeType));
i++;
if(i <= nodeTypes.size()) {
spellcheckerQuery += " UNION ";
}
}
return spellcheckerQuery;
}
/**
* Sets a new fulltext query that will be executed.
*
* @param query the fulltext query.
*/
public void setQuery(String query) {
this.query = query;
// if the query string is surrounded by double quotes and does not contain any other double quote
// (e.g. "foo bar"), we don't parse the string and use it as it is
if (query != null && query.length() > 2 && query.startsWith("\"") && query.endsWith("\"")
&& !query.substring(1, query.length()-1).contains("\"")) {
search.setQuery(query);
return;
}
if (!StringUtils.isEmpty(query)) {
// remove queries that contain only special characters
query = query.replaceAll("^" + SPECIAL_CHARS_REGEX + "$", "");
if (!StringUtils.isEmpty(query)) {
// remove negation
query = query.replaceAll("^" + SPECIAL_CHARS_REGEX + "-", "");
}
}
// use GQL to filter out wildcards
try {
final StringBuilder sb = new StringBuilder();
GQL.parse(query, request.getResourceResolver().adaptTo(Session.class),
new GQL.ParserCallback() {
public void term(String property, String value, boolean optional)
throws RepositoryException {
sb.append(" ");
if (optional) {
sb.append("OR ");
}
if (property.length() > 0) {
sb.append(property).append(":");
}
sb.append(value);
}
});
search.setQuery(sb.toString().trim());
} catch (RepositoryException e) {
search.setQuery("");
}
}
/**
* @return the names of the properties that will be used in an excerpt.
*
* @deprecated since 5.2. Excerpt properties can now only be specified in
* the configuration of the QueryBuilder interface. For 5.3,
* when Jackrabbit 1.5 is used, the excerpt properties can be
* configured in the repository.
*
*/
@Deprecated
public String getExcerptPropertyNames() {
return "";
}
/**
* @param properties
* comma separated names of the properties that will be used in
* an excerpt.
*
* @deprecated since 5.2. Excerpt properties can now only be specified in
* the configuration of the QueryBuilder interface. For 5.3,
* when Jackrabbit 1.5 is used, the excerpt properties can be
* configured in the repository.
*
*/
@Deprecated
public void setExcerptPropertyNames(String properties) {
}
/**
* @return the number of hits to display per page.
*/
public long getHitsPerPage() {
return search.getHitsPerPage();
}
/**
* @param num the number of hits to display on a page.
*/
public void setHitsPerPage(long num) {
search.setHitsPerPage(num);
}
/**
* @return the location where to search in.
*/
public String getSearchIn() {
return search.getSearchIn();
}
/**
* @param searchIn the location where to search in.
*/
public void setSearchIn(String searchIn) {
search.setSearchIn(searchIn);
}
/**
* @return the names of the properties that will be searched.
*/
public String getSearchProperties() {
return search.getSearchProperties();
}
/**
* @param properties comma separated names of the properties that will be searched.
*/
public void setSearchProperties(String properties) {
search.setSearchProperties(properties);
}
/**
* A search result.
*/
public final class Result {
/**
* The underyling CQ search result instance.
*/
private final SearchResult result;
/**
* The hits on the current result page.
*/
private final List hits;
/**
* The spell suggestion. Set only when requested. An empty string
* indicates that a suggestion was returned by the spellcheck, but
* does not return results for the current query.
* See {@link #getSpellcheck()}.
*/
private String spellSuggestion;
/**
* Creates a new result based on the given CQ search result.
*
* @param result the CQ search result.
*/
private Result(SearchResult result) {
this.result = result;
this.hits = new ArrayList();
for (com.day.cq.search.result.Hit h : result.getHits()) {
this.hits.add(new Hit(h));
}
}
/**
* @return a List of {@link Page}es to display the navigation through the
* result pages.
* @throws RepositoryException if an error occurs while reading from
* the query result.
*/
public List getResultPages() throws RepositoryException {
if (resultPages == null) {
resultPages = new ArrayList();
for (com.day.cq.search.result.ResultPage rp : result.getResultPages()) {
resultPages.add(new Page(rp));
}
}
return resultPages;
}
/**
* @return the page, which contains the information about the previous page.
* Returns null
if there is no previous page (i.e. the
* current page is the first page).
* @throws RepositoryException if an error occurs while reading from
* the query result.
*/
public Page getPreviousPage() throws RepositoryException {
ResultPage previous = result.getPreviousPage();
if (previous != null) {
return new Page(previous);
} else {
return null;
}
}
/**
* @return the page, which contains the information about the next page.
* Returns null
if there is no next page (i.e. the
* current page is the last page).
* @throws RepositoryException if an error occurs while reading from
* the query result.
*/
public Page getNextPage() throws RepositoryException {
ResultPage next = result.getNextPage();
if (next != null) {
return new Page(next);
} else {
return null;
}
}
/**
* @return the script for query and result tracking.
*/
public String getTrackerScript() {
StringBuffer sb = new StringBuffer();
String contextPath = request.getContextPath();
sb.append("");
return sb.toString();
}
/**
* @return the result of a spell check or null
if spell
* checking is not supported or the repository thinks the spelling
* is correct.
*/
public String getSpellcheck() {
if (spellSuggestion == null) {
try {
Session session = request.getResourceResolver().adaptTo(Session.class);
RowIterator rows = session.getWorkspace().getQueryManager().createQuery(getSpellcheckerQuery(), Query.JCR_SQL2).execute().getRows();
String suggestion = null;
if (rows.hasNext()) {
Value v = rows.nextRow().getValue("rep:spellcheck()");
if (v != null) {
suggestion = v.getString();
}
}
if (suggestion == null) {
return null;
}
// set query term to suggestion and run query again
search.setQuery(suggestion);
if (search.getResult().getTotalMatches() > 0) {
spellSuggestion = suggestion;
} else {
spellSuggestion = "";
}
} catch (RepositoryException e) {
spellSuggestion = "";
}
}
if (spellSuggestion.length() == 0) {
return null;
} else {
return spellSuggestion;
}
}
/**
* @return the start index. i.e. from where to start to display the hits.
*/
public long getStartIndex() {
return result.getStartIndex();
}
/**
* @return the total number of matches.
*/
public long getTotalMatches() {
return result.getTotalMatches();
}
/**
* Returns the execution time in fractions of a second.
*
* Example: 0.08 (means, the query took 80 milliseconds to execute).
* @return the execution time of the query.
*/
public String getExecutionTime() {
return result.getExecutionTime();
}
/**
* Returns the execution time in milliseconds.
*
* @return the execution time of the query.
*/
public long getExecutionTimeMillis() {
return result.getExecutionTimeMillis();
}
/**
* Returns the facets for this search result.
*
* @return the facets for this search result.
* @throws RepositoryException if an error occurs while executing the query
* or calculating the facets.
*/
public Map getFacets() throws RepositoryException {
return result.getFacets();
}
/**
* @return a List of {@link Hit}s to display on the result page.
*/
public List getHits() {
return hits;
}
}
/**
* A hit within the search result.
*/
public final class Hit {
/**
* The underlying CQ hit.
*/
private final com.day.cq.search.result.Hit hit;
/**
* Creates a new hit based on the given CQ hit
.
*
* @param hit the CQ hit to wrap.
*/
private Hit(com.day.cq.search.result.Hit hit) {
this.hit = hit;
}
/**
* Returns the title for this hit. The returned string may contain
* HTML tags, which means it must not be escaped when written to the
* response.
*
* @return the title for this hit.
* @throws RepositoryException if an error occurs while reading form the
* repository.
*/
public String getTitle() throws RepositoryException {
String excerpt = hit.getExcerpts().get(JcrConstants.JCR_TITLE);
if (excerpt != null) {
return excerpt;
}
return getPageOrAsset().getName();
}
/**
* @return the default excerpt for this hit.
* @throws RepositoryException if an error occurs while building the
* excerpt.
*/
public String getExcerpt() throws RepositoryException {
return hit.getExcerpt();
}
/**
* @return the url for this hit.
* @throws RepositoryException if an error occurs while reading from the
* query result.
*/
public String getURL() throws RepositoryException {
Node n = getPageOrAsset();
String url = request.getContextPath() + n.getPath();
if (isPage(n)) {
url += "." + HTML_EXT;
}
return url;
}
/**
* @return the url that displays similar pages for this hit.
* @throws RepositoryException if an error occurs while reading from
* the query result.
*/
public String getSimilarURL() throws RepositoryException {
StringBuffer url = new StringBuffer();
url.append(request.getRequestURI());
url.append("?").append(QUERY_PARAM_NAME);
url.append("=").append(encodeURL(SimpleSearch.RELATED_PREFIX));
url.append(encodeURL(hit.getPath()));
return url.toString();
}
/**
* Returns an icon image for this hit.
*
* @return an icon image for this hit.
* @throws RepositoryException if an error occurs while reading from
* the repository.
*/
public String getIcon() throws RepositoryException {
String url = getURL();
int idx = url.lastIndexOf('.');
if (idx == -1) {
// no extension
return "";
}
String ext = url.substring(idx + 1);
if (ext.equals(HTML_EXT)) {
// do not provide an icon for html
return "";
}
String path = getIconPath(ext);
if (path == null) {
return "";
}
StringBuffer sb = new StringBuffer();
sb.append("");
return sb.toString();
}
/**
* Returns the extension of the url of this hit.
* @return the extension
* @throws RepositoryException if an error occurs
*/
public String getExtension() throws RepositoryException {
String url = getURL();
int idx = url.lastIndexOf('.');
return idx >=0
? url.substring(idx+1)
: "";
}
/**
* @return the content properties on this hit.
* @throws RepositoryException if an error occurs while reading from the
* repository.
*/
public Map getProperties() throws RepositoryException {
return hit.getProperties();
}
/**
* Returns true
if the given node is either of type
* cq:Page
, cq:PseudoPage
or
* dam:Asset
.
*
* @param n the node to check.
* @return true
if the node represents either a page or an
* asset; false
otherwise.
* @throws RepositoryException if an error occurs during the check.
*/
private boolean isPageOrAsset(Node n) throws RepositoryException {
return isPage(n) || n.isNodeType(DamConstants.NT_DAM_ASSET);
}
/**
* Returns true
if the given node is either of type
* cq:Page
or cq:PseudoPage
.
*
* @param n the node to check.
* @return true
if the node represents a page;
* false
otherwise.
* @throws RepositoryException if an error occurs during the check.
*/
private boolean isPage(Node n) throws RepositoryException {
return n.isNodeType(NameConstants.NT_PAGE)
|| n.isNodeType(NameConstants.NT_PSEUDO_PAGE);
}
/**
* Returns the page or asset node that contains the current hit node.
*
* @return the page or asset node that contains the current hit node.
* @throws RepositoryException if an error occurs while reading from the
* repository.
*/
private Node getPageOrAsset() throws RepositoryException {
Node n = hit.getNode();
while (!isPageOrAsset(n) && n.getName().length() > 0) {
n = n.getParent();
}
return n;
}
}
/**
* A result page.
*/
public final class Page {
/**
* The underlying CQ result page.
*/
private final ResultPage rp;
/**
* Creates a new page with the given CQ result page.
*
* @param rp the CQ result page.
*/
private Page(ResultPage rp) {
this.rp = rp;
}
/**
* @return whether this page is currently displayed.
*/
public boolean isCurrentPage() {
return rp.isCurrentPage();
}
/**
* @return zero based index of this result page.
*/
public long getIndex() {
return rp.getIndex();
}
/**
* @return the URL to this search result page.
*/
public String getURL() {
StringBuffer url = new StringBuffer();
url.append(request.getRequestURI());
url.append("?").append(QUERY_PARAM_NAME);
url.append("=").append(encodeURL(search.getQuery()));
url.append("&").append(START_PARAM_NAME);
url.append("=").append(rp.getStart());
String lang = request.getParameter(LANGUAGE_FACET_PARAM_NAME);
if (lang != null) {
url.append("&").append(LANGUAGE_FACET_PARAM_NAME);
url.append("=").append(lang);
}
String tag = request.getParameter(TAG_FACET_PARAM_NAME);
if (tag != null) {
url.append("&").append(TAG_FACET_PARAM_NAME);
url.append("=").append(tag);
}
String mimeType = request.getParameter(MIME_TYPE_FACET_PARAM_NAME);
if (mimeType != null) {
url.append("&").append(MIME_TYPE_FACET_PARAM_NAME);
url.append("=").append(mimeType);
}
String from = request.getParameter(FROM_FACET_PARAM_NAME);
if (from != null) {
url.append("&").append(FROM_FACET_PARAM_NAME);
url.append("=").append(from);
}
String to = request.getParameter(TO_FACET_PARAM_NAME);
if (to != null) {
url.append("&").append(TO_FACET_PARAM_NAME);
url.append("=").append(to);
}
return url.toString();
}
}
/**
* Encodes the given url
using {@link URLEncoder#encode(String, String)}
* with a UTF-8
encoding.
*
* @param url the url to encode.
* @return the encoded url.
*/
private static String encodeURL(String url) {
try {
return URLEncoder.encode(url, "UTF-8");
} catch (UnsupportedEncodingException e) {
// will never happen
return url;
}
}
/**
* Returns the icon path for the given extension or null
if
* none is found for the given extension.
*
* @param extension the extension.
* @return the icon path for the given extension or null
.
*/
private String getIconPath(String extension) {
Component c = WCMUtils.getComponent(resource);
if (c == null) {
return null;
}
Resource icon = c.getLocalResource("resources/" + extension + ".gif");
if (icon == null) {
icon = c.getLocalResource("resources/default.gif");
}
return icon == null ? null : icon.getPath();
}
private List getNodeTypes() {
RequestParameter[] nodeTypeParams = request.getRequestParameters(NODE_TYPE_PARAM_NAME);
if (nodeTypeParams != null && nodeTypeParams.length > 0) {
List nodeTypes = new ArrayList();
for (RequestParameter parameter : nodeTypeParams) {
String type = parameter.getString();
nodeTypes.add(type);
}
return nodeTypes;
} else {
return defaultNodeTypes;
}
}
private void addNodeTypesPredicate(List types) {
PredicateGroup nodeTypesPredicate = new PredicateGroup("nodeTypes");
for (String type : types) {
if (StringUtils.isNotEmpty(type)) {
Predicate predicate = new Predicate("type", "type");
predicate.set("type", type);
nodeTypesPredicate.add(predicate);
}
}
nodeTypesPredicate.setAllRequired(false); // type1 OR type2
this.search.addPredicate(nodeTypesPredicate);
log.info("Searching with the type(s): {}", Arrays.toString(types.toArray()));
}
}