View Javadoc
1   /**
2    * This file Copyright (c) 2015-2016 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.templating.functions;
35  
36  import static com.google.common.base.Preconditions.checkArgument;
37  
38  import info.magnolia.cms.security.PermissionUtil;
39  import info.magnolia.context.MgnlContext;
40  import info.magnolia.jcr.util.ContentMap;
41  import info.magnolia.jcr.util.NodeTypes;
42  import info.magnolia.jcr.wrapper.I18nNodeWrapper;
43  import info.magnolia.repository.RepositoryConstants;
44  
45  import java.util.ArrayList;
46  import java.util.Collection;
47  import java.util.List;
48  
49  import javax.inject.Singleton;
50  import javax.jcr.Node;
51  import javax.jcr.RepositoryException;
52  import javax.jcr.Session;
53  import javax.jcr.query.Query;
54  import javax.jcr.query.QueryManager;
55  import javax.jcr.query.QueryResult;
56  import javax.jcr.query.Row;
57  import javax.jcr.query.RowIterator;
58  
59  import org.apache.commons.lang3.StringUtils;
60  import org.apache.jackrabbit.JcrConstants;
61  import org.slf4j.Logger;
62  import org.slf4j.LoggerFactory;
63  
64  /**
65   * Search related functions especially suitable for being used in templates. They return a collection of {@link ContentMap} which is more easily accessible in template scripts via . (dot) notation.
66   * Results are sorted by jcr:score() descending. An excerpt from the text containing the query term might be available, depending on the <a href="http://wiki.apache.org/jackrabbit/ExcerptProvider">Jackrabbit search configuration</a>.
67   * Read permissions for the current user are checked before adding the result to the collection returned.
68   * Functions are exposed as <code>searchfn</code>.
69   * <p>
70   * Here follows a couple of sample usages.
71   *
72   * <pre>
73   * [#assign searchResults = searchfn.searchPages('foo', '/somepath') /]
74   * 
75   * [#if searchResults?has_content]
76   *   [#list searchResults as item]
77   *   &lt;a href="${cmsfn.link(item)}"&gt;
78   *      &lt;h4&gt;${item.title!}&lt;/h4&gt;
79   *      &lt;p&gt;${item.excerpt!}&lt;/p&gt;
80   *   &lt;/a&gt;
81   *   [/#list]
82   * [/#if]
83   * </pre>
84   *
85   * and
86   *
87   * <pre>
88   * [#assign searchResults = searchfn.searchContent('myworkspace', 'foo', '/somepath','qux:baz') /]
89   * 
90   * [#if searchResults?has_content]
91   *   [#list searchResults as item]
92   *   &lt;a href="${cmsfn.link(item)}"&gt;
93   *      &lt;h4&gt;${item.title!}&lt;/h4&gt;
94   *      &lt;p&gt;${item.excerpt!}&lt;/p&gt;
95   *   &lt;/a&gt;
96   *   [/#list]
97   * [/#if]
98   *
99   * </pre>
100  */
101 @Singleton
102 public class SearchTemplatingFunctions {
103 
104     private static final Logger log = LoggerFactory.getLogger(SearchTemplatingFunctions.class);
105 
106     private static final String SEARCH_QUERY_PATTERN = "SELECT rep:excerpt() from %s WHERE jcr:path like '%s/%%' AND contains(., '%s') ORDER BY jcr:score() DESC";
107 
108     /**
109      * Searches the {@link RepositoryConstants#WEBSITE} for {@link NodeTypes.Page#NAME}.
110      *
111      * @see #searchPages(String, String, long, long)
112      */
113     public Collection<ContentMap> searchPages(String queryString, String startPath) throws RepositoryException {
114         return searchPages(queryString, startPath, Integer.MAX_VALUE, 0);
115     }
116 
117     /**
118      * Searches the {@link RepositoryConstants#WEBSITE} for {@link NodeTypes.Page#NAME}.
119      *
120      * @see #searchContent(String, String, String, String, long, long)
121      */
122     public Collection<ContentMap> searchPages(String queryString, String startPath, long limit, long offset) throws RepositoryException {
123         return searchContent(RepositoryConstants.WEBSITE, queryString, startPath, NodeTypes.Page.NAME, limit, offset);
124     }
125 
126     /**
127      * Searches content in any workspace.
128      *
129      * @param returnItemType if not specified, defaults to <em>nt:base</em>
130      * @see #searchContent(String, String, String, String, long, long)
131      */
132     public Collection<ContentMap> searchContent(String workspace, String queryString, String startPath, String returnItemType) throws RepositoryException {
133         return searchContent(workspace, queryString, startPath, returnItemType, Integer.MAX_VALUE, 0);
134     }
135 
136     /**
137      * Searches content in any workspace.
138      *
139      * @see #searchContent(String, String, String, String)
140      */
141     public Collection<ContentMap> searchContent(String workspace, String queryString, String startPath, String returnItemType, long limit, long offset) throws RepositoryException {
142 
143         checkArgument(StringUtils.isNotBlank(workspace), "workspace can't be null or empty");
144         checkArgument(StringUtils.isNotBlank(queryString), "queryString can't be null or empty");
145         checkArgument(StringUtils.isNotBlank(startPath), "startPath can't be null or empty");
146         checkArgument(limit > 0, "limit was %s but it must be greater than 0", limit);
147         checkArgument(offset >= 0, "offset was %s but it must be greater than or equal to 0", offset);
148 
149         List<ContentMap> result = new ArrayList<ContentMap>();
150 
151         final Session jcrSession = MgnlContext.getJCRSession(workspace);
152         final QueryManager jcrQueryManager = jcrSession.getWorkspace().getQueryManager();
153         // excerpt functionality isn't supported in JCR_SQL2, see https://issues.apache.org/jira/browse/JCR-3609
154         final Query query = jcrQueryManager.createQuery(String.format(SEARCH_QUERY_PATTERN, StringUtils.defaultIfBlank(returnItemType, JcrConstants.NT_BASE), startPath, queryString), Query.SQL);
155         query.setLimit(limit);
156         query.setOffset(offset);
157 
158         log.debug("Executing query against workspace [{}] with statement [{}] and limit {} and offset {}...", workspace, query.getStatement(), limit, offset);
159         long start = System.currentTimeMillis();
160         QueryResult qr = query.execute();
161         log.debug("Query execution took {} ms", System.currentTimeMillis() - start);
162         RowIterator it = qr.getRows();
163 
164         while (it.hasNext()) {
165             Row row = it.nextRow();
166             Node node = row.getNode();
167             if (PermissionUtil.isGranted(jcrSession, node.getPath(), Session.ACTION_READ)) {
168                 String excerpt = hasExcerptValue(row) ? row.getValue("rep:excerpt()").getString() : "";
169                 node.setProperty("excerpt", excerpt);
170                 result.add(new ContentMap(new I18nNodeWrapper(node)));
171             }
172         }
173 
174         return result;
175 
176     }
177 
178     private boolean hasExcerptValue(Row row) {
179         try {
180             row.getValue("rep:excerpt()");
181             return true;
182         } catch (RepositoryException e) {
183         }
184         return false;
185     }
186 }