View Javadoc

1   /**
2    * This file Copyright (c) 2003-2010 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.cms.taglibs.util;
35  
36  import info.magnolia.cms.beans.config.ContentRepository;
37  import info.magnolia.cms.core.Content;
38  import info.magnolia.cms.core.ItemType;
39  import info.magnolia.cms.core.search.Query;
40  import info.magnolia.cms.core.search.QueryResult;
41  import info.magnolia.context.MgnlContext;
42  import org.apache.commons.lang.ArrayUtils;
43  import org.apache.commons.lang.StringUtils;
44  import org.slf4j.Logger;
45  import org.slf4j.LoggerFactory;
46  
47  import javax.jcr.RepositoryException;
48  import javax.servlet.jsp.JspException;
49  import javax.servlet.jsp.PageContext;
50  import javax.servlet.jsp.tagext.TagSupport;
51  import java.text.MessageFormat;
52  
53  
54  /**
55   * A simple tag which allows searching in all the site content with a "natural language" query.
56   * It simply strips all the reserved chars from input string, build an xpath query and feed Magnolia QueryManager.
57   * By defaults search terms are ANDed, but it also supports using the AND or OR keywords in the query string.
58   * Search is not case sensitive and it's performed on any non-binary property.
59   * A collection on Content (page) objects is added to the specified scope with the specified name.
60   *
61   * @jsp.tag name="simpleSearch" body-content="empty"
62   * @jsp.tag-example
63   * <cmsu:simplesearch query="${param.search}" var="results" />
64   *   <c:forEach items="${results}" var="page">
65   *   <a href="${pageContext.request.contextPath}${page.handle}.html">${page.title}</a>
66   * </c:forEach>
67   *
68   * @author Fabrizio Giustina
69   * @version $Revision: 32667 $ ($Author: gjoseph $)
70   */
71  public class SimpleSearchTag extends TagSupport {
72  
73      /**
74       * Stable serialVersionUID.
75       */
76      private static final long serialVersionUID = 222L;
77  
78      /**
79       * Reserved chars, stripped from query.
80       */
81      private static final String RESERVED_CHARS = "()[]{}<>:/\\@*?=\"'&"; //$NON-NLS-1$
82  
83      /**
84       * keywords.
85       */
86      static final String[] KEYWORDS = new String[]{"and", "or"}; //$NON-NLS-1$ //$NON-NLS-2$
87  
88      private static final Logger log = LoggerFactory.getLogger(SimpleSearchTag.class);
89  
90      private int startLevel;
91      private String query;
92      private String var;
93      private String repository = ContentRepository.WEBSITE;
94      private String itemType = ItemType.CONTENT.getSystemName();
95      private boolean supportSubstringSearch = false;
96      private boolean useSimpleJcrQuery = true;
97      private String startPath;
98      private int scope = PageContext.PAGE_SCOPE;
99  
100     /**
101      * Query to execute (e.g. "magnolia AND cms OR info")
102      * @jsp.attribute required="true" rtexprvalue="true"
103      */
104     public void setQuery(String query) {
105         this.query = query;
106     }
107 
108     /**
109      * The search results (a collection of Content nodes (pages)) will be added to the pagecontext using this name.
110      * @jsp.attribute required="true" rtexprvalue="true"
111      */
112     public void setVar(String var) {
113         this.var = var;
114     }
115 
116     /**
117      * Scope for the variable. Can be "page" (default), "request", "session", "application".
118      * @jsp.attribute required="false" rtexprvalue="true"
119      */
120     public void setScope(String scope) {
121         if ("request".equalsIgnoreCase(scope)) { //$NON-NLS-1$
122             this.scope = PageContext.REQUEST_SCOPE;
123         }
124         else if ("session".equalsIgnoreCase(scope)) { //$NON-NLS-1$
125             this.scope = PageContext.SESSION_SCOPE;
126         }
127         else if ("application".equalsIgnoreCase(scope)) { //$NON-NLS-1$
128             this.scope = PageContext.APPLICATION_SCOPE;
129         }
130         else {
131             // default
132             this.scope = PageContext.PAGE_SCOPE;
133         }
134     }
135 
136     /**
137      * The start level for search, defaults to 0. Can be used to limit the search only to the current website tree.
138      * @jsp.attribute required="false" rtexprvalue="true" type="int"
139      */
140     public void setStartLevel(int startLevel) {
141         this.startLevel = startLevel;
142     }
143 
144     public int doStartTag() throws JspException {
145 
146         final String queryString = useSimpleJcrQuery ? generateSimpleQuery(query) : generateComplexXPathQuery();
147 
148         if (queryString == null) {
149             if (log.isDebugEnabled()) {
150                 log.debug("A valid query could not be built, skipping"); //$NON-NLS-1$
151             }
152             return EVAL_PAGE;
153         }
154 
155         if (log.isDebugEnabled()) {
156             log.debug("Executing xpath query " + queryString); //$NON-NLS-1$
157         }
158 
159         Query q;
160         try {
161             q = MgnlContext.getQueryManager(repository).createQuery(queryString, "xpath"); //$NON-NLS-1$
162 
163             QueryResult result = q.execute();
164 
165             pageContext.setAttribute(var, result.getContent(itemType), scope);
166         }
167         catch (Exception e) {
168             log.error(MessageFormat.format(
169                 "{0} caught while parsing query for search term [{1}] - query is [{2}]: {3}", //$NON-NLS-1$
170                 new Object[]{e.getClass().getName(), this.query, queryString, e.getMessage()}), e);
171         }
172 
173         return EVAL_PAGE;
174     }
175 
176     /**
177      * This generates a simple jcr:contains query.
178      *
179      * @see "6.6.5.2 jcr:contains Function" from the JCR Spec (pages 110-111) for details.
180      */
181     protected String generateSimpleQuery(String input) {
182         final String startPathToUse = startPath();
183 
184         // jcr and xpath escaping :
185         final String escapedQuery = input.replaceAll("'", "\\\\''");
186         final String queryString = startPathToUse + "//*[@jcr:primaryType='mgnl:content']//*[jcr:contains(., '" + escapedQuery + "')]";
187         log.debug("query string: " + queryString);
188         return queryString;
189     }
190 
191     protected String startPath() {
192         String cleanStartPath = null;
193         // search only in a specific subtree
194         if (this.startLevel > 0) {
195             try {
196                 Content activePage = MgnlContext.getAggregationState().getMainContent();
197                 if (activePage != null) {
198                     cleanStartPath = activePage.getAncestor(this.startLevel).getHandle();
199                 }
200             } catch (RepositoryException e) {
201                 log.error(e.getMessage(), e);
202             }
203         } else {
204             cleanStartPath = this.startPath;
205         }
206         return StringUtils.defaultIfEmpty(StringUtils.strip(cleanStartPath, "/"), "");
207     }
208 
209     /**
210      * @deprecated as from 3.5.5, this query is deemed to complex and not properly working, since it
211      * forces a search on non-indexed word. The better generateSimpleQuery() method is recommened.
212      */
213     protected String generateComplexXPathQuery() {
214         return generateXPathQuery();
215     }
216 
217     /**
218      * Split search terms and build an xpath query in the form:
219      * <code>//*[@jcr:primaryType='mgnl:content']/\*\/\*[jcr:contains(., 'first') or jcr:contains(., 'second')]</code>
220      * @return valid xpath expression or null if the given query doesn't contain at least one valid search term
221      *
222      * @deprecated as from 3.5.5, this query is deemed to complex and not properly working, since it
223      * forces a search on non-indexed word. The better generateSimpleQuery() method is recommened.
224      */
225     protected String generateXPathQuery() {
226         // strip reserved chars and split
227         String[] tokens = StringUtils.split(StringUtils.lowerCase(StringUtils.replaceChars(
228             this.query,
229             RESERVED_CHARS,
230             null)));
231 
232         // null input string?
233         if (tokens == null) {
234             return null;
235         }
236 
237         StringBuffer xpath = new StringBuffer(tokens.length * 20);
238         xpath.append(startPath());
239         xpath.append("//*[@jcr:primaryType=\'mgnl:content\']//*["); //$NON-NLS-1$
240 
241         String joinOperator = "and"; //$NON-NLS-1$
242         boolean emptyQuery = true;
243 
244         for (int j = 0; j < tokens.length; j++) {
245             String tkn = tokens[j];
246             if (ArrayUtils.contains(KEYWORDS, tkn)) {
247                 joinOperator = tkn;
248             }
249             else {
250                 if (!emptyQuery) {
251                     xpath.append(" "); //$NON-NLS-1$
252                     xpath.append(joinOperator);
253                     xpath.append(" "); //$NON-NLS-1$
254                 }
255                 xpath.append("jcr:contains(., '"); //$NON-NLS-1$
256                 if(supportSubstringSearch){
257                     xpath.append("*");
258                     xpath.append(tkn);
259                     xpath.append("*");
260                 }
261                 else{
262                     xpath.append(tkn);
263                 }
264 
265                 xpath.append("')"); //$NON-NLS-1$
266                 emptyQuery = false;
267             }
268 
269         }
270 
271         xpath.append("]"); //$NON-NLS-1$
272 
273         // if no valid search terms are added don't return a catch-all query
274         if (emptyQuery) {
275             return null;
276         }
277 
278         return xpath.toString();
279     }
280 
281     /**
282      * @see javax.servlet.jsp.tagext.TagSupport#release()
283      */
284     public void release() {
285         this.query = null;
286         this.var = null;
287         this.scope = PageContext.PAGE_SCOPE;
288         this.startLevel = 0;
289         this.startPath = null;
290         this.itemType = null;
291         this.repository = null;
292         this.supportSubstringSearch = false;
293         this.useSimpleJcrQuery = true;
294         super.release();
295     }
296 
297 
298     public String getRepository() {
299         return this.repository;
300     }
301 
302     /**
303      * The repository we search in. Default is website repository.
304      * @jsp.attribute required="false" rtexprvalue="true"
305      */
306     public void setRepository(String repository) {
307         this.repository = repository;
308     }
309 
310     public boolean isSupportSubstringSearch() {
311         return this.supportSubstringSearch;
312     }
313 
314     /**
315      * Search for substrings too. This can decrease performance. Default value is false.
316      * @deprecated not used when useSimpleJcrQuery is set to true.
317      * @jsp.attribute required="false" rtexprvalue="true" type="boolean"
318      */
319     public void setSupportSubstringSearch(boolean supportSubstringSearch) {
320         this.supportSubstringSearch = supportSubstringSearch;
321     }
322 
323     /**
324      * Set this attribute to false to generate the search query as it was generated until Magnolia 3.5.4
325      * (which will force a search on non-indexed word, which usually leads in less good results).
326      * As from 3.5.5, this is true by default, and generates simpler and better queries.
327      * See "6.6.5.2 jcr:contains Function" from the JCR Spec (pages 110-111) for details.
328      * @jsp.attribute required="false" rtexprvalue="true" type="boolean"
329      */
330     public void setUseSimpleJcrQuery(boolean useSimpleJcrQuery) {
331         this.useSimpleJcrQuery = useSimpleJcrQuery;
332     }
333 
334     /**
335      * @return the itemType
336      */
337     public String getItemType() {
338         return this.itemType;
339     }
340 
341     /**
342      * The itemTypes search/returned by this tag. Default is mgnl:content which is used for pages.
343      * @jsp.attribute required="false" rtexprvalue="true"
344      */
345     public void setItemType(String itemType) {
346         this.itemType = itemType;
347     }
348 
349 
350     public String getStartPath() {
351         return this.startPath;
352     }
353 
354     /**
355      * The path we search in.
356      * @jsp.attribute required="false" rtexprvalue="true"
357      */
358     public void setStartPath(String startPath) {
359         this.startPath = startPath;
360     }
361 
362 }