View Javadoc

1   /**
2    * This file Copyright (c) 2008-2014 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.module.templatingkit.search;
35  
36  import info.magnolia.cms.core.Content;
37  import info.magnolia.cms.core.ItemType;
38  import info.magnolia.cms.core.NodeData;
39  import info.magnolia.cms.util.ContentUtil;
40  import info.magnolia.jcr.util.NodeTypes;
41  import info.magnolia.jcr.util.PropertyUtil;
42  import info.magnolia.module.templatingkit.templates.category.TemplateCategoryUtil;
43  import info.magnolia.templating.functions.TemplatingFunctions;
44  
45  import java.util.ArrayList;
46  import java.util.Calendar;
47  import java.util.Collection;
48  import java.util.Iterator;
49  import java.util.List;
50  import java.util.regex.Pattern;
51  
52  import javax.jcr.Node;
53  import javax.jcr.PropertyType;
54  import javax.jcr.RepositoryException;
55  
56  import org.apache.commons.lang.StringUtils;
57  import org.slf4j.Logger;
58  import org.slf4j.LoggerFactory;
59  
60  /**
61   * Search Result Item Bean implementation. Used by STK SearchResultModel.
62   * FIXME: Should be refactored to use only Node and Property.
63   *   Missing core implementation: Utility to get a collection of filtered properties (Primitive and Binary) of
64   *   a Node and Node Chield.
65   *   See {@link info.magnolia.cms.core.DefaultContent}
66   *      .getNodeDataCollection()
67   *      .getChildren()
68   */
69  public class SearchResultItem {
70      private static final Logger log = LoggerFactory.getLogger(SearchResultItem.class);
71  
72      private static final Pattern HTML_STRIP = Pattern.compile("<.*?>", Pattern.DOTALL);
73  
74      /**
75       * Number of chars to include in result.
76       */
77      protected int chars = 100;
78  
79      /**
80       * Maximum number of snippets to include in result.
81       */
82      protected int maxSnippets = 3;
83  
84      private Calendar date;
85      private String author;
86      private String category;
87      private String title;
88      private String text;
89      private Node node;
90      private String link;
91      protected String query;
92  
93  
94      /**
95       * FIXME usage of deprecated classes: {@link ContentUtil}.
96       * To remove when core has implemented I18nContentSupport for Node
97       */
98      public SearchResultItem(Node node, String query, TemplatingFunctions templatingFunctions) {
99          this.query = query;
100         this.node = node;
101         this.title = PropertyUtil.getString(node, "title");
102         this.author = PropertyUtil.getString(node, "author");
103 
104         this.category = StringUtils.capitalize(TemplateCategoryUtil.getTemplateSubCategory(node));
105         if(StringUtils.isEmpty(category)){
106             this.category = StringUtils.capitalize(TemplateCategoryUtil.getTemplateCategory(node));
107         }
108 
109         this.date = PropertyUtil.getDate(node, "date");
110         if(date == null){
111             try {
112                 this.date = NodeTypes.LastModified.getLastModified(node);
113             } catch (RepositoryException e) {
114                 // we'll leave it as null
115             }
116         }
117 
118         this.link = templatingFunctions.link(node);
119         this.text = this.getSnippets();
120     }
121 
122     public SearchResultItem(Node node, String query, String title, String author, String category, Calendar date, String link) {
123         this.query = query;
124         this.node = node;
125         this.title = title;
126         this.author = author;
127 
128         this.category = category;
129         this.date = date;
130         this.link = link;
131 
132         this.text = this.getSnippets();
133     }
134 
135     public Calendar getDate() {
136         return date;
137     }
138 
139     public String getAuthor() {
140         return author;
141     }
142 
143     public String getCategory() {
144         return category;
145     }
146 
147     public String getTitle() {
148         return title;
149     }
150 
151     public String getText() {
152         return text;
153     }
154 
155     /**
156      * @deprecated since stk1.1
157      */
158     public String getFormattedText() {
159         return text;
160     }
161 
162     public String getLink() {
163         return link;
164     }
165 
166     public Node getNode() {
167         return node;
168     }
169 
170     public String getSnippets() {
171 
172         log.debug("collecting snippets");
173 
174         StringBuffer snippets = new StringBuffer();
175         int numberOfSnippets = 0;
176 
177         boolean endProcess = false;
178         if(processNodeDatas(numberOfSnippets, snippets, ContentUtil.asContent(node))) {
179 
180             Collection paragraphCollections = ContentUtil.asContent(node).getChildren(ItemType.CONTENTNODE);
181 
182             Iterator iterator = paragraphCollections.iterator();
183             while (iterator.hasNext() && !endProcess) {
184                 Content paragraphCollection = (Content) iterator.next();
185                 if(!processNodeDatas(numberOfSnippets, snippets, paragraphCollection )) {
186                     endProcess = true;
187                     break;
188                 }
189                 Collection<Content> paragraphs = paragraphCollection.getChildren(ItemType.CONTENTNODE);
190 
191                 for (Content paragraph : paragraphs) {
192                     if (!processNodeDatas(numberOfSnippets, snippets, paragraph)) {
193                         endProcess = true;
194                         break;
195                     }
196 
197                     log.debug("Iterating on paragraph {}", paragraph); //$NON-NLS-1$
198 
199 
200                 }
201             }
202         }
203 
204         return snippets.toString();
205     }
206 
207 
208     public int getMaxSnippets() {
209         return maxSnippets;
210     }
211 
212     public void setMaxSnippets(int maxSnippets) {
213         this.maxSnippets = maxSnippets;
214     }
215 
216 
217     /**
218      * FIXME usage of deprecated classes: {@link ContentUtil}.
219      * To remove when core has implemented getNodeDataCollection() for Node
220      */
221     protected boolean processNodeDatas(int numberOfSnippets, StringBuffer snippets, Content paragraph) {
222         //TODO: code extracted from SearchResultSnippetTag
223         Collection<NodeData> properties = paragraph.getNodeDataCollection();
224         List<String> snippetList = new ArrayList<String>();
225 
226         Iterator<NodeData> dataIterator = properties.iterator();
227         while (dataIterator.hasNext()) {
228             NodeData property = dataIterator.next();
229             if (property.getType() != PropertyType.BINARY) {
230                 highlightSnippet(snippetList, property);
231                 if (snippetList.size() + numberOfSnippets >= maxSnippets) {
232                     break;
233                 }
234             }
235         }
236         for (String snippet : snippetList) {
237             snippets.append(snippet);
238         }
239 
240         return snippetList.size() + numberOfSnippets < maxSnippets;
241     }
242 
243     protected void highlightSnippet(List<String> snippetList, NodeData property) {
244         String resultString = property.getString();
245 
246         log.debug("Iterating on property {}", property.getName()); //$NON-NLS-1$
247         log.debug("Property value is {}", resultString); //$NON-NLS-1$
248 
249         String searchTerm = this.query;
250 
251         if (skipProperty(property.getName(), resultString, searchTerm)) {
252             return;
253         }
254 
255         // exclude words with less than 2 chars
256         if ( searchTerm != null && searchTerm.length() > 2) {
257 
258             log.debug("Looking for search term [{}] in [{}]", searchTerm, resultString);
259 
260             // if search term was wrapped in quotas, remove them now
261             if (StringUtils.startsWith(searchTerm, "\"") && StringUtils.endsWith(searchTerm, "\"")) {
262                 searchTerm = searchTerm.substring(1,searchTerm.length() - 1);
263             }
264 
265             // first check, avoid using heavy string replaceAll operations if the search term is not
266             // there
267             if (!StringUtils.contains(resultString.toLowerCase(), searchTerm.toLowerCase())) {
268                 return;
269             }
270 
271             // strips out html tags using a regexp
272             resultString = stripHtmlTags(resultString);
273 
274             // only get first matching keyword
275             int pos = resultString.toLowerCase().indexOf(searchTerm.toLowerCase());
276             if (pos > -1) {
277 
278                 int posEnd = pos + searchTerm.length();
279                 int from = (pos - chars / 2);
280                 if (from < 0) {
281                     from = 0;
282                 }
283 
284                 int to = from + chars;
285                 if (to > resultString.length()) {
286                     to = resultString.length();
287                 }
288 
289                 StringBuffer snippet = new StringBuffer();
290 
291                 snippet.append(StringUtils.substring(resultString, from, pos));
292                 snippet.append("<em class=\"highlight\">");
293                 snippet.append(StringUtils.substring(resultString, pos, posEnd));
294                 snippet.append("</em>");
295                 snippet.append(StringUtils.substring(resultString, posEnd, to));
296 
297                 if (from > 0) {
298                     snippet.insert(0, "... ");
299                 }
300                 if (to < resultString.length()) {
301                     snippet.append("... ");
302                 }
303 
304                 log.debug("Search term found, adding snippet {}", snippet);
305 
306                 snippetList.add(snippet + "<br />");
307             }
308         }
309     }
310 
311     protected boolean skipProperty(String name, String resultString, String searchTerm) {
312         // a quick and buggy way to avoid configuration properties, we should allow the user to
313         // configure a list of nodeData to search for...
314         return resultString.length() < 20;
315     }
316 
317     protected String stripHtmlTags(String resultString) {
318         return HTML_STRIP.matcher(resultString).replaceAll("");
319     }
320 
321 
322 }