1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 package info.magnolia.periscope.search.jcr;
35
36 import static java.util.stream.Collectors.joining;
37
38 import info.magnolia.context.MgnlContext;
39 import info.magnolia.jcr.iterator.FilteringPropertyIterator;
40 import info.magnolia.jcr.predicate.JCRMgnlPropertyHidingPredicate;
41 import info.magnolia.jcr.util.NodeTypes;
42 import info.magnolia.jcr.util.NodeUtil;
43 import info.magnolia.jcr.util.PropertyUtil;
44 import info.magnolia.periscope.operation.request.InternalNavigationRequest;
45 import info.magnolia.periscope.search.SearchException;
46 import info.magnolia.periscope.search.SearchQuery;
47 import info.magnolia.periscope.search.SearchResult;
48 import info.magnolia.periscope.search.SearchResultSupplier;
49 import info.magnolia.periscope.tag.PeriscopeTagsProvider;
50
51 import java.time.ZonedDateTime;
52 import java.time.format.DateTimeFormatter;
53 import java.util.StringJoiner;
54 import java.util.stream.Stream;
55
56 import javax.jcr.Node;
57 import javax.jcr.NodeIterator;
58 import javax.jcr.Property;
59 import javax.jcr.PropertyIterator;
60 import javax.jcr.PropertyType;
61 import javax.jcr.RepositoryException;
62 import javax.jcr.Session;
63 import javax.jcr.query.Query;
64 import javax.jcr.query.QueryManager;
65
66 import org.apache.commons.collections4.CollectionUtils;
67 import org.apache.commons.lang3.StringUtils;
68 import org.apache.jackrabbit.util.Text;
69 import org.jsoup.Jsoup;
70
71 import lombok.SneakyThrows;
72
73
74
75
76 public class JcrSearchResultSupplier implements SearchResultSupplier {
77
78 private final String name;
79 private final JcrSearchResultSupplierDefinition definition;
80 private final PeriscopeTagsProvider periscopeTagsProvider;
81
82 public JcrSearchResultSupplier(String name, JcrSearchResultSupplierDefinition definition, PeriscopeTagsProvider periscopeTagsProvider) {
83 this.name = name;
84 this.definition = definition;
85 this.periscopeTagsProvider = periscopeTagsProvider;
86 }
87
88 public String getWorkspace() {
89 return definition.getWorkspace();
90 }
91
92 @Override
93 public String getName() {
94 return this.name;
95 }
96
97 @Override
98 @SneakyThrows(RepositoryException.class)
99 public Stream<SearchResult> search(SearchQuery query) throws SearchException {
100
101 NodeIterator searchResults = runQuery(query);
102 Stream.Builder<SearchResult> builder = Stream.builder();
103
104 while (searchResults.hasNext()) {
105 Node node = searchResults.nextNode();
106
107 if (definition.getNodeTypes().contains(node.getPrimaryNodeType().getName())) {
108 builder.add(createResult(query, node));
109 }
110 }
111 return builder.build().distinct();
112 }
113
114 private NodeIterator runQuery(SearchQuery query) throws RepositoryException {
115 final String jcrQueryString = constructJCRQuery(query);
116
117 Session session = MgnlContext.getJCRSession(definition.getWorkspace());
118 QueryManager manager = session.getWorkspace().getQueryManager();
119
120 Query jcrQuery = manager.createQuery(jcrQueryString, Query.JCR_SQL2);
121 jcrQuery.setLimit(definition.getResultLimit());
122
123 return NodeUtil.filterDuplicates(jcrQuery.execute().getNodes());
124 }
125
126 private String constructJCRQuery(SearchQuery query) {
127 final String escapedQuery = Text.escapeIllegalJcrChars(query.getQuery());
128 String lowerCaseQuery = escapedQuery.toLowerCase();
129 StringBuilder searchQuery = new StringBuilder();
130
131
132 String nodeTypesQuery = definition.getNodeTypes().stream()
133 .map(this::getConditionBasedOnNodeType)
134 .collect(joining(" OR "));
135
136 searchQuery.append("SELECT * FROM ").append("[nt:base] AS t")
137 .append(" WHERE ((lower(LOCALNAME()) LIKE ").append("'%").append(lowerCaseQuery).append("%'");
138
139
140 if (!"/".equals(definition.getRootPath())) {
141 searchQuery.append(" AND ISDESCENDANTNODE('").append(definition.getRootPath()).append("')");
142 }
143
144 if (StringUtils.isNotBlank(definition.getTitleProperty())) {
145 searchQuery.append(String.format(" OR lower(\"%s\") LIKE ", definition.getTitleProperty())).append("'%").append(lowerCaseQuery).append("%'");
146 }
147 searchQuery.append(") AND (").append(nodeTypesQuery).append(")");
148
149 if (StringUtils.isNotBlank(escapedQuery)) {
150 if (definition.isFullTextSearch()) {
151 searchQuery.append(" OR ").append("CONTAINS(t.*, '*").append(lowerCaseQuery).append("*')");
152 }
153 if (definition.isPropertySearch()) {
154 searchQuery.append(" OR t.['").append(escapedQuery).append("'] IS NOT NULL");
155 }
156 }
157 searchQuery.append(")");
158
159 if (StringUtils.isNotEmpty(periscopeTagsProvider.getTagsProperty()) && CollectionUtils.isNotEmpty(query.getTags())) {
160 StringJoiner clauses = new StringJoiner(" OR ");
161 query.getTags().forEach(tag -> clauses.add("[" + periscopeTagsProvider.getTagsProperty() + "] = '" + tag + "'"));
162 searchQuery.append(" AND ").append("(").append(clauses).append(")");
163 }
164
165 if (CollectionUtils.isNotEmpty(query.getEditors())) {
166 StringJoiner clauses = new StringJoiner(" OR ");
167 query.getEditors().forEach(editor -> clauses.add("[" + NodeTypes.LastModified.LAST_MODIFIED_BY + "] = '" + editor + "'"));
168 searchQuery.append(" AND ").append("(").append(clauses).append(")");
169 }
170
171 if (query.getStartDate() != null) {
172 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS");
173 String date = query.getStartDate().format(formatter);
174 searchQuery.append(" AND ").append("[").append(NodeTypes.LastModified.LAST_MODIFIED).append("]").append(" > ").append("CAST('").append(date).append("' AS DATE)");
175 }
176
177 if (query.getEndDate() != null) {
178 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS");
179 String date = query.getEndDate().format(formatter);
180 searchQuery.append(" AND ").append("[").append(NodeTypes.LastModified.LAST_MODIFIED).append("]").append(" < ").append("CAST('").append(date).append("' AS DATE)");
181 }
182
183 return searchQuery.toString();
184 }
185
186 private String getConditionBasedOnNodeType(String nodeType) {
187 return String.format("t.[jcr:primaryType] = '%s'", nodeType);
188 }
189
190 private SearchResult createResult(SearchQuery query, Node node) throws RepositoryException {
191 final String title = StringUtils.isBlank(definition.getTitleProperty()) ? node.getName()
192 : PropertyUtil.getString(node, definition.getTitleProperty(), node.getName());
193
194 boolean titleMatchesQuery = query.getQuery().isEmpty() || StringUtils.containsIgnoreCase(title, query.getQuery());
195 String content = titleMatchesQuery ? "" : Jsoup.parse(aggregatedContent(node)).text();
196
197
198
199
200
201 String lastModified = PropertyUtil.getString(node, NodeTypes.LastModified.LAST_MODIFIED);
202 String appPath = NodeUtil.getPathIfPossible(node);
203 if (!definition.getRootPath().equals("/")) {
204 appPath = StringUtils.substringAfter(appPath, definition.getRootPath());
205 }
206
207 return SearchResult.builder()
208 .title(title)
209 .content(content)
210 .path(appPath)
211 .operationRequest(new InternalNavigationRequest(definition.getAppName(), definition.getWorkspace(), appPath))
212 .type(StringUtils.defaultIfBlank(definition.getIcon(), "icon-node-content"))
213 .lastModifiedBy(PropertyUtil.getString(node, NodeTypes.LastModified.LAST_MODIFIED_BY))
214 .lastModified((StringUtils.isNotBlank(lastModified)) ? ZonedDateTime.parse(lastModified) : null)
215 .build();
216 }
217
218 private String aggregatedContent(Node rootNode) throws RepositoryException {
219 StringBuilder aggregatedContentBuilder = new StringBuilder();
220
221 NodeUtil.visit(rootNode, node -> {
222 for (PropertyIterator iter = new FilteringPropertyIterator(node.getProperties(), new JCRMgnlPropertyHidingPredicate()); iter.hasNext(); ) {
223 Property property = iter.nextProperty();
224 if (property.getType() == PropertyType.STRING && !property.isMultiple()) {
225 String propertyValue = property.getString().trim();
226 if (property.getName().equals("extends") && !propertyValue.isEmpty()) {
227 aggregatedContentBuilder.append("extends ");
228 }
229 aggregatedContentBuilder.append(propertyValue);
230 aggregatedContentBuilder.append(" ");
231 }
232 }
233 });
234
235 return aggregatedContentBuilder.toString().trim();
236 }
237 }