View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package org.mycore.solr.search;
20  
21  import static org.mycore.solr.MCRSolrConstants.SOLR_CONFIG_PREFIX;
22  
23  import java.util.ArrayList;
24  import java.util.HashMap;
25  import java.util.HashSet;
26  import java.util.Iterator;
27  import java.util.List;
28  import java.util.Locale;
29  import java.util.Map;
30  import java.util.Set;
31  import java.util.stream.Collectors;
32  
33  import org.apache.logging.log4j.LogManager;
34  import org.apache.logging.log4j.Logger;
35  import org.apache.solr.client.solrj.SolrQuery;
36  import org.apache.solr.client.solrj.SolrQuery.ORDER;
37  import org.apache.solr.client.solrj.SolrQuery.SortClause;
38  import org.mycore.common.MCRException;
39  import org.mycore.common.config.MCRConfiguration2;
40  import org.mycore.parsers.bool.MCRAndCondition;
41  import org.mycore.parsers.bool.MCRCondition;
42  import org.mycore.parsers.bool.MCRNotCondition;
43  import org.mycore.parsers.bool.MCROrCondition;
44  import org.mycore.parsers.bool.MCRSetCondition;
45  import org.mycore.services.fieldquery.MCRQueryCondition;
46  import org.mycore.services.fieldquery.MCRSortBy;
47  import org.mycore.solr.MCRSolrConstants;
48  import org.mycore.solr.MCRSolrUtils;
49  
50  /**
51   * @author Thomas Scheffler (yagee)
52   * @author Jens Kupferschmidt
53   *
54   */
55  public class MCRConditionTransformer {
56      private static final Logger LOGGER = LogManager.getLogger(MCRConditionTransformer.class);
57  
58      /**
59       * If a condition references fields from multiple indexes, this constant is
60       * returned
61       */
62      protected static final String MIXED = "--mixed--";
63  
64      private static HashSet<String> joinFields = null;
65  
66      public static String toSolrQueryString(@SuppressWarnings("rawtypes") MCRCondition condition,
67          Set<String> usedFields) {
68          return toSolrQueryString(condition, usedFields, false).toString();
69      }
70  
71      public static boolean explicitAndOrMapping() {
72          return MCRConfiguration2.getBoolean("MCR.Solr.ConditionTransformer.ExplicitAndOrMapping").orElse(false);
73      }
74  
75      @SuppressWarnings({ "unchecked", "rawtypes" })
76      private static StringBuilder toSolrQueryString(MCRCondition condition, Set<String> usedFields,
77          boolean subCondition) {
78          if (condition instanceof MCRQueryCondition) {
79              MCRQueryCondition qCond = (MCRQueryCondition) condition;
80              return handleQueryCondition(qCond, usedFields);
81          }
82          if (condition instanceof MCRSetCondition) {
83              MCRSetCondition<MCRCondition> setCond = (MCRSetCondition<MCRCondition>) condition;
84              return handleSetCondition(setCond, usedFields, subCondition);
85          }
86          if (condition instanceof MCRNotCondition) {
87              MCRNotCondition notCond = (MCRNotCondition) condition;
88              return handleNotCondition(notCond, usedFields);
89          }
90          throw new MCRException("Cannot handle MCRCondition class: " + condition.getClass().getCanonicalName());
91      }
92  
93      private static StringBuilder handleQueryCondition(MCRQueryCondition qCond, Set<String> usedFields) {
94          String field = qCond.getFieldName();
95          String value = qCond.getValue();
96          String operator = qCond.getOperator();
97          usedFields.add(field);
98          switch (operator) {
99          case "like":
100         case "contains":
101             return getTermQuery(field, value.trim());
102         case "=":
103         case "phrase":
104             return getPhraseQuery(field, value);
105         case "<":
106             return getLTQuery(field, value);
107         case "<=":
108             return getLTEQuery(field, value);
109         case ">":
110             return getGTQuery(field, value);
111         case ">=":
112             return getGTEQuery(field, value);
113         }
114         throw new UnsupportedOperationException("Do not know how to handle operator: " + operator);
115     }
116 
117     @SuppressWarnings("rawtypes")
118     private static StringBuilder handleSetCondition(MCRSetCondition<MCRCondition> setCond, Set<String> usedFields,
119         boolean subCondition) {
120         if (explicitAndOrMapping()) {
121             return handleSetConditionExplicit(setCond, usedFields);
122         } else {
123             return handleSetConditionDefault(setCond, usedFields, subCondition);
124         }
125     }
126 
127     @SuppressWarnings("rawtypes")
128     private static StringBuilder handleSetConditionExplicit(MCRSetCondition<MCRCondition> setCond,
129         Set<String> usedFields) {
130         List<MCRCondition<MCRCondition>> children = setCond.getChildren();
131         if (children.isEmpty()) {
132             return null;
133         }
134         StringBuilder sb = new StringBuilder();
135         sb.append("(");
136         Iterator<MCRCondition<MCRCondition>> iterator = children.iterator();
137         StringBuilder subSb = toSolrQueryString(iterator.next(), usedFields, true);
138         sb.append(subSb);
139         while (iterator.hasNext()) {
140             sb.append(' ').append(setCond.getOperator().toUpperCase(Locale.ROOT)).append(' ');
141             subSb = toSolrQueryString(iterator.next(), usedFields, true);
142             sb.append(subSb);
143         }
144         sb.append(")");
145         return sb;
146     }
147 
148     @SuppressWarnings("rawtypes")
149     private static StringBuilder handleSetConditionDefault(MCRSetCondition<MCRCondition> setCond,
150         Set<String> usedFields,
151         boolean subCondition) {
152         boolean stripPlus;
153         if (setCond instanceof MCROrCondition) {
154             stripPlus = true;
155         } else if (setCond instanceof MCRAndCondition) {
156             stripPlus = false;
157         } else {
158             throw new UnsupportedOperationException("Do not know how to handle "
159                 + setCond.getClass().getCanonicalName() + " set operation.");
160         }
161         List<MCRCondition<MCRCondition>> children = setCond.getChildren();
162         if (children.isEmpty()) {
163             return null;
164         }
165         StringBuilder sb = new StringBuilder();
166         boolean groupRequired = subCondition || setCond instanceof MCROrCondition;
167         if (groupRequired) {
168             sb.append("+(");
169         }
170         Iterator<MCRCondition<MCRCondition>> iterator = children.iterator();
171         StringBuilder subSb = toSolrQueryString(iterator.next(), usedFields, true);
172         sb.append(stripPlus ? stripPlus(subSb) : subSb);
173         while (iterator.hasNext()) {
174             sb.append(" ");
175             subSb = toSolrQueryString(iterator.next(), usedFields, true);
176             sb.append(stripPlus ? stripPlus(subSb) : subSb);
177         }
178         if (groupRequired) {
179             sb.append(")");
180         }
181         return sb;
182     }
183 
184     @SuppressWarnings("rawtypes")
185     private static StringBuilder handleNotCondition(MCRNotCondition notCond, Set<String> usedFields) {
186         MCRCondition child = notCond.getChild();
187         StringBuilder sb = new StringBuilder();
188         sb.append("-");
189         StringBuilder solrQueryString = toSolrQueryString(child, usedFields, true);
190         if (!explicitAndOrMapping()) {
191             stripPlus(solrQueryString);
192         }
193         if (solrQueryString == null || solrQueryString.length() == 0) {
194             return null;
195         }
196         sb.append(solrQueryString);
197         return sb;
198     }
199 
200     private static StringBuilder getRangeQuery(String field, String lowerTerm, boolean includeLower, String upperTerm,
201         boolean includeUpper) {
202         StringBuilder sb = new StringBuilder();
203         sb.append('+');
204         sb.append(field);
205         sb.append(":");
206         sb.append(includeLower ? '[' : '{');
207         sb.append(
208             lowerTerm != null ? ("*".equals(lowerTerm) ? "\\*" : MCRSolrUtils.escapeSearchValue(lowerTerm)) : "*");
209         sb.append(" TO ");
210         sb.append(
211             upperTerm != null ? ("*".equals(upperTerm) ? "\\*" : MCRSolrUtils.escapeSearchValue(upperTerm)) : "*");
212         sb.append(includeUpper ? ']' : '}');
213         return sb;
214     }
215 
216     public static StringBuilder getLTQuery(String field, String value) {
217         return getRangeQuery(field, null, true, value, false);
218     }
219 
220     public static StringBuilder getLTEQuery(String field, String value) {
221         return getRangeQuery(field, null, true, value, true);
222     }
223 
224     public static StringBuilder getGTQuery(String field, String value) {
225         return getRangeQuery(field, value, false, null, true);
226     }
227 
228     public static StringBuilder getGTEQuery(String field, String value) {
229         return getRangeQuery(field, value, true, null, true);
230     }
231 
232     public static StringBuilder getTermQuery(String field, String value) {
233         if (value.length() == 0) {
234             return null;
235         }
236         StringBuilder sb = new StringBuilder();
237         if (!explicitAndOrMapping()) {
238             sb.append('+');
239         }
240         sb.append(field);
241         sb.append(":");
242         String replaced = value.replaceAll("\\s+", " AND ");
243         if (value.length() == replaced.length()) {
244             sb.append(MCRSolrUtils.escapeSearchValue(value));
245         } else {
246             sb.append("(");
247             sb.append(MCRSolrUtils.escapeSearchValue(replaced));
248             sb.append(")");
249         }
250         return sb;
251     }
252 
253     public static StringBuilder getPhraseQuery(String field, String value) {
254         StringBuilder sb = new StringBuilder();
255         if (!explicitAndOrMapping()) {
256             sb.append('+');
257         }
258         sb.append(field);
259         sb.append(":");
260         sb.append('"');
261         sb.append(MCRSolrUtils.escapeSearchValue(value));
262         sb.append('"');
263         return sb;
264     }
265 
266     private static StringBuilder stripPlus(StringBuilder sb) {
267         if (sb == null || sb.length() == 0) {
268             return sb;
269         }
270         if (sb.charAt(0) == '+') {
271             sb.deleteCharAt(0);
272         }
273         return sb;
274     }
275 
276     public static SolrQuery getSolrQuery(@SuppressWarnings("rawtypes") MCRCondition condition, List<MCRSortBy> sortBy,
277         int maxResults, List<String> returnFields) {
278         String queryString = getQueryString(condition);
279         SolrQuery q = applySortOptions(new SolrQuery(queryString), sortBy);
280         q.setIncludeScore(true);
281         q.setRows(maxResults == 0 ? Integer.MAX_VALUE : maxResults);
282 
283         if (returnFields != null) {
284             q.setFields(returnFields.size() > 0 ? returnFields.stream().collect(Collectors.joining(",")) : "*");
285         }
286         String sort = q.getSortField();
287         LOGGER.info("MyCoRe Query transformed to: {}{} {}", q.getQuery(), sort != null ? " " + sort : "",
288             q.getFields());
289         return q;
290     }
291 
292     public static String getQueryString(@SuppressWarnings("rawtypes") MCRCondition condition) {
293         Set<String> usedFields = new HashSet<>();
294         return MCRConditionTransformer.toSolrQueryString(condition, usedFields);
295     }
296 
297     public static SolrQuery applySortOptions(SolrQuery q, List<MCRSortBy> sortBy) {
298         for (MCRSortBy option : sortBy) {
299             SortClause sortClause = new SortClause(option.getFieldName(), option.getSortOrder() ? ORDER.asc
300                 : ORDER.desc);
301             q.addSort(sortClause);
302         }
303         return q;
304     }
305 
306     /**
307      * Builds SOLR query.
308      * 
309      * Automatically builds JOIN-Query if content search fields are used in query.
310      * @param sortBy sort criteria
311      * @param not true, if all conditions should be negated
312      * @param and AND or OR connective between conditions  
313      * @param table conditions per "content" or "metadata"
314      * @param maxHits maximum hits
315      */
316     @SuppressWarnings("rawtypes")
317     public static SolrQuery buildMergedSolrQuery(List<MCRSortBy> sortBy, boolean not, boolean and,
318         HashMap<String, List<MCRCondition>> table, int maxHits, List<String> returnFields) {
319         List<MCRCondition> queryConditions = table.get("metadata");
320         MCRCondition combined = buildSubCondition(queryConditions, and, not);
321         SolrQuery solrRequestQuery = getSolrQuery(combined, sortBy, maxHits, returnFields);
322 
323         for (Map.Entry<String, List<MCRCondition>> mapEntry : table.entrySet()) {
324             if (!mapEntry.getKey().equals("metadata")) {
325                 MCRCondition combinedFilterQuery = buildSubCondition(mapEntry.getValue(), and, not);
326                 SolrQuery filterQuery = getSolrQuery(combinedFilterQuery, sortBy, maxHits, returnFields);
327                 solrRequestQuery.addFilterQuery(MCRSolrConstants.SOLR_JOIN_PATTERN + filterQuery.getQuery());
328             }
329         }
330         return solrRequestQuery;
331     }
332 
333     /** Builds a new condition for all fields from one single index */
334     @SuppressWarnings({ "rawtypes", "unchecked" })
335     protected static MCRCondition buildSubCondition(List<MCRCondition> conditions, boolean and, boolean not) {
336         MCRCondition subCond;
337         if (conditions.size() == 1) {
338             subCond = conditions.get(0);
339         } else if (and) {
340             subCond = new MCRAndCondition().addAll(conditions);
341         } else {
342             subCond = new MCROrCondition().addAll(conditions);
343         }
344         if (not) {
345             subCond = new MCRNotCondition(subCond);
346         }
347         return subCond;
348     }
349 
350     /**
351      * Build a table from index ID to a List of conditions referencing this
352      * index
353      */
354     @SuppressWarnings("rawtypes")
355     public static HashMap<String, List<MCRCondition>> groupConditionsByIndex(MCRSetCondition cond) {
356         HashMap<String, List<MCRCondition>> table = new HashMap<>();
357         @SuppressWarnings("unchecked")
358         List<MCRCondition> children = cond.getChildren();
359 
360         for (MCRCondition child : children) {
361             String index = getIndex(child);
362             table.computeIfAbsent(index, k -> new ArrayList<>()).add(child);
363         }
364         return table;
365     }
366 
367     /**
368      * Returns the ID of the index of all fields referenced in this condition.
369      * If the fields come from multiple indexes, the constant mixed is returned.
370      */
371     @SuppressWarnings("rawtypes")
372     private static String getIndex(MCRCondition cond) {
373         if (cond instanceof MCRQueryCondition) {
374             MCRQueryCondition queryCondition = ((MCRQueryCondition) cond);
375             String fieldName = queryCondition.getFieldName();
376             return getIndex(fieldName);
377         } else if (cond instanceof MCRNotCondition) {
378             return getIndex(((MCRNotCondition) cond).getChild());
379         }
380 
381         @SuppressWarnings("unchecked")
382         List<MCRCondition> children = ((MCRSetCondition) cond).getChildren();
383 
384         // mixed indexes here!
385         return children.stream()
386             .map(MCRConditionTransformer::getIndex)
387             .reduce((l, r) -> l.equals(r) ? l : MIXED)
388             .get();
389     }
390 
391     public static String getIndex(String fieldName) {
392         return getJoinFields().contains(fieldName) ? "content" : "metadata";
393     }
394 
395     private static HashSet<String> getJoinFields() {
396         if (joinFields == null) {
397             joinFields = MCRConfiguration2.getString(SOLR_CONFIG_PREFIX + "JoinQueryFields")
398                 .stream()
399                 .flatMap(MCRConfiguration2::splitValue)
400                 .collect(Collectors.toCollection(HashSet::new));
401         }
402         return joinFields;
403     }
404 
405 }