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.Arrays;
25  import java.util.Collections;
26  import java.util.HashMap;
27  import java.util.HashSet;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Map.Entry;
31  import java.util.Set;
32  import java.util.StringTokenizer;
33  
34  import org.apache.logging.log4j.LogManager;
35  import org.apache.logging.log4j.Logger;
36  import org.mycore.common.config.MCRConfiguration2;
37  import org.mycore.common.config.MCRConfigurationException;
38  import org.mycore.frontend.servlets.MCRServlet;
39  import org.mycore.frontend.servlets.MCRServletJob;
40  import org.mycore.solr.proxy.MCRSolrProxyServlet;
41  
42  import jakarta.servlet.RequestDispatcher;
43  import jakarta.servlet.ServletException;
44  import jakarta.servlet.http.HttpServletRequest;
45  import jakarta.servlet.http.HttpServletResponse;
46  
47  /**
48   * Used to map a formular-post to a solr request.
49   * <p>
50   * <b>Parameters</b>
51   * </p>
52   * <dl>
53   * <dt><strong>Solr reserved parameters</strong></dt>
54   * <dd>They will directly forwarded to the server.</dd>
55   * <dt><strong>Type parameters</strong></dt>
56   * <dd>They are used to join other documents in the search. They start with
57   * "solr.type.".</dd>
58   * <dt><strong>Sort parameters</strong></dt>
59   * <dd>They are used to sort the results in the right order. They start with
60   * "sort."</dd>
61   * <dt><strong>Query parameters</strong></dt>
62   * <dd>They are used to build the query for solr. All parameters which are not
63   * reserved, type or sort parameters will be stored here.</dd>
64   * </dl>
65   * @author mcrshofm
66   * @author mcrsherm
67   */
68  
69  public class MCRSolrSearchServlet extends MCRServlet {
70  
71      private static final long serialVersionUID = 1L;
72  
73      private enum QueryType {
74          phrase, term
75      }
76  
77      private enum SolrParameterGroup {
78          QueryParameter, SolrParameter, SortParameter, TypeParameter
79      }
80  
81      private static final Logger LOGGER = LogManager.getLogger(MCRSolrSearchServlet.class);
82  
83      private static final String JOIN_PATTERN = "{!join from=returnId to=id}";
84  
85      private static final String PHRASE_QUERY_PARAM = "solr.phrase";
86  
87      /** Parameters that can be used within a select request to solr */
88      static final List<String> RESERVED_PARAMETER_KEYS;
89  
90      static {
91          String[] parameter = { "q", "qt", "sort", "start", "rows", "pageDoc", "pageScore", "fq", "cache",
92              "fl", "glob",
93              "debug", "explainOther", "defType", "timeAllowed", "omitHeader", "sortOrder", "sortBy", "wt", "qf", "q.alt",
94              "mm", "pf",
95              "ps", "qs", "tie", "bq", "bf", "lang", "facet", "facet.field", "facet.sort", "facet.method",
96              PHRASE_QUERY_PARAM, };
97  
98          RESERVED_PARAMETER_KEYS = Collections.unmodifiableList(Arrays.asList(parameter));
99      }
100 
101     /**
102      * Adds a field with all values to a {@link StringBuilder} An empty field
103      * value will be skipped.
104      *
105      * @param query
106      *            represents a solr query
107      * @param fieldValues
108      *            containing all the values for the field
109      * @param fieldName
110      *            the name of the field
111      * @throws ServletException
112      */
113     private void addFieldToQuery(StringBuilder query, String[] fieldValues, String fieldName, QueryType queryType)
114         throws ServletException {
115         for (String fieldValue : fieldValues) {
116             if (fieldValue.length() == 0) {
117                 continue;
118             }
119             switch (queryType) {
120             case term:
121                 query.append(MCRConditionTransformer.getTermQuery(fieldName, fieldValue));
122                 break;
123             case phrase:
124                 query.append(MCRConditionTransformer.getPhraseQuery(fieldName, fieldValue));
125                 break;
126             default:
127                 throw new ServletException("Query type is unsupported: " + queryType);
128             }
129             query.append(' ');
130         }
131     }
132 
133     /**
134      * @param queryParameters
135      *            all parameter where
136      *            <code>getParameterGroup.equals(QueryParameter)</code>
137      * @param typeParameters
138      *            all parameter where
139      *            <code>getParameterGroup.equals(TypeParameter)</code>
140      * @return a map which can be forwarded to {@link MCRSolrProxyServlet}
141      */
142     protected Map<String, String[]> buildSelectParameterMap(Map<String, String[]> queryParameters,
143         Map<String, String[]> typeParameters,
144         Map<String, String[]> sortParameters, Set<String> phraseQuery) throws ServletException {
145         HashMap<String, String[]> queryParameterMap = new HashMap<>();
146 
147         HashMap<String, String> fieldTypeMap = createFieldTypeMap(typeParameters);
148 
149         HashMap<String, StringBuilder> filterQueryMap = new HashMap<>();
150         StringBuilder query = new StringBuilder();
151         for (Entry<String, String[]> queryParameter : queryParameters.entrySet()) {
152             String fieldName = queryParameter.getKey();
153             String[] fieldValues = queryParameter.getValue();
154             QueryType queryType = phraseQuery.contains(fieldName) ? QueryType.phrase : QueryType.term;
155             // Build the q parameter without solr.type.fields
156             if (!fieldTypeMap.containsKey(fieldName)) {
157                 addFieldToQuery(query, fieldValues, fieldName, queryType);
158 
159             } else {
160                 String fieldType = fieldTypeMap.get(fieldName);
161                 StringBuilder filterQueryBuilder = getFilterQueryBuilder(filterQueryMap, fieldType);
162                 addFieldToQuery(filterQueryBuilder, fieldValues, fieldName, queryType);
163             }
164         }
165 
166         // put query and all filterquery´s to the map
167         queryParameterMap.put("q", new String[] { query.toString().trim() });
168 
169         for (StringBuilder filterQueryBuilder : filterQueryMap.values()) {
170             // skip the whole query if no field has been added
171             if (filterQueryBuilder.length() > JOIN_PATTERN.length()) {
172                 queryParameterMap.put("fq", new String[] { filterQueryBuilder.toString() });
173             }
174         }
175 
176         queryParameterMap.put("sort", new String[] { buildSolrSortParameter(sortParameters) });
177 
178         return queryParameterMap;
179     }
180 
181     /**
182      *
183      * @param sortParameters
184      * @return
185      */
186     private String buildSolrSortParameter(Map<String, String[]> sortParameters) {
187         Set<Entry<String, String[]>> sortParameterEntrys = sortParameters.entrySet();
188         Map<Integer, String> positionOrderMap = new HashMap<>();
189         Map<Integer, String> positionFieldMap = new HashMap<>();
190 
191         for (Entry<String, String[]> sortParameterEntry : sortParameterEntrys) {
192             StringTokenizer st = new StringTokenizer(sortParameterEntry.getKey(), ".");
193             st.nextToken(); // skip sort.
194             Integer position = Integer.parseInt(st.nextToken());
195             String type = st.nextToken();
196             String[] valueArray = sortParameterEntry.getValue();
197             if (valueArray.length > 0) {
198                 String value = valueArray[0];
199                 if ("order".equals(type)) {
200                     positionOrderMap.put(position, value);
201                 } else if ("field".equals(type)) {
202                     positionFieldMap.put(position, value);
203                 }
204             }
205         }
206 
207         ArrayList<Integer> sortedPositions = new ArrayList<>();
208 
209         sortedPositions.addAll(positionFieldMap.keySet());
210         Collections.sort(sortedPositions);
211 
212         StringBuilder sortBuilder = new StringBuilder();
213         for (Integer position : sortedPositions) {
214             sortBuilder.append(",");
215             sortBuilder.append(positionFieldMap.get(position));
216             String order = positionOrderMap.get(position);
217             sortBuilder.append(" ");
218             if (order == null) {
219                 order = "asc";
220                 LOGGER.warn("No sort order found for field with number ''{}'' use default value : ''{}''", position,
221                     order);
222             }
223             sortBuilder.append(order);
224         }
225         if (sortBuilder.length() != 0) {
226             sortBuilder.deleteCharAt(0);
227         }
228 
229         return sortBuilder.toString();
230     }
231 
232     /**
233      * This method is used to create a map wich contains all fields as key and
234      * the type of the field as value.
235      *
236      * @param typeParameters
237      * @return
238      */
239     private HashMap<String, String> createFieldTypeMap(Map<String, String[]> typeParameters) {
240         HashMap<String, String> fieldTypeMap = new HashMap<>();
241 
242         for (Entry<String, String[]> currentType : typeParameters.entrySet()) {
243             for (String typeMember : currentType.getValue()) {
244                 fieldTypeMap.put(typeMember, currentType.getKey());
245             }
246         }
247         return fieldTypeMap;
248     }
249 
250     @Override
251     protected void doGetPost(MCRServletJob job) throws Exception {
252         Map<String, String[]> solrParameters = new HashMap<>();
253         Map<String, String[]> queryParameters = new HashMap<>();
254         Map<String, String[]> typeParameters = new HashMap<>();
255         Map<String, String[]> sortParameters = new HashMap<>();
256         Set<String> phraseQuery = new HashSet<>();
257         String[] phraseFields = job.getRequest().getParameterValues(PHRASE_QUERY_PARAM);
258         if (phraseFields != null) {
259             phraseQuery.addAll(Arrays.asList(phraseFields));
260         }
261 
262         HttpServletRequest request = job.getRequest();
263         HttpServletResponse response = job.getResponse();
264 
265         extractParameterList(request.getParameterMap(), queryParameters, solrParameters, typeParameters,
266             sortParameters);
267         Map<String, String[]> buildedSolrParameters = buildSelectParameterMap(queryParameters, typeParameters,
268             sortParameters, phraseQuery);
269         buildedSolrParameters.putAll(solrParameters);
270 
271         request.setAttribute(MCRSolrProxyServlet.MAP_KEY, buildedSolrParameters);
272         LOGGER.info("Forward SOLR Parameters: {}", buildedSolrParameters);
273         RequestDispatcher requestDispatcher = getServletContext().getRequestDispatcher("/servlets/SolrSelectProxy");
274         requestDispatcher.forward(request, response);
275     }
276 
277     @Override
278     public void init() throws ServletException {
279         super.init();
280     }
281 
282     /**
283      * Splits the parameters into three groups.
284      *
285      * @param requestParameter
286      *            the map of parameters to split.
287      * @param queryParameter
288      *            all querys will be stored here.
289      * @param solrParameter
290      *            all solr-parameters will be stored here.
291      * @param typeParameter
292      *            all type-parameters will be stored here.
293      * @param sortParameter
294      *            all sort-parameters will be stored here.
295      */
296     protected void extractParameterList(Map<String, String[]> requestParameter, Map<String, String[]> queryParameter,
297         Map<String, String[]> solrParameter, Map<String, String[]> typeParameter, Map<String, String[]> sortParameter) {
298         for (Entry<String, String[]> currentEntry : requestParameter.entrySet()) {
299             String parameterName = currentEntry.getKey();
300             if (PHRASE_QUERY_PARAM.equals(parameterName)) {
301                 continue;
302             }
303             SolrParameterGroup parameterGroup = getParameterType(parameterName);
304 
305             switch (parameterGroup) {
306             case SolrParameter:
307                 solrParameter.put(parameterName, currentEntry.getValue());
308                 break;
309             case TypeParameter:
310                 typeParameter.put(parameterName, currentEntry.getValue());
311                 break;
312             case QueryParameter:
313                 String[] strings = currentEntry.getValue();
314                 for (String v : strings) {
315                     if (v != null && v.length() > 0) {
316                         queryParameter.put(parameterName, currentEntry.getValue());
317                     }
318                 }
319                 break;
320             case SortParameter:
321                 sortParameter.put(parameterName, currentEntry.getValue());
322                 break;
323             default:
324                 LOGGER.warn("Unknown parameter group. That should not happen.");
325                 continue;
326             }
327         }
328     }
329 
330     /**
331      * @param filterQueryMap
332      *            a map wich contains all {@link StringBuilder}
333      * @param fieldType
334      * @return a {@link StringBuilder} for the specific fieldType
335      */
336     private StringBuilder getFilterQueryBuilder(HashMap<String, StringBuilder> filterQueryMap, String fieldType) {
337         if (!filterQueryMap.containsKey(fieldType)) {
338             filterQueryMap.put(fieldType, new StringBuilder(JOIN_PATTERN));
339         }
340         return filterQueryMap.get(fieldType);
341     }
342 
343     /**
344      * Returns the {@link SolrParameterGroup} for a specific parameter name.
345      *
346      * @param parameterName
347      *            the name of the parameter
348      * @return the parameter group enum
349      */
350     private SolrParameterGroup getParameterType(String parameterName) {
351         if (isTypeParameter(parameterName)) {
352             LOGGER.debug("Parameter {} is a {}", parameterName, SolrParameterGroup.TypeParameter.toString());
353             return SolrParameterGroup.TypeParameter;
354         } else if (isSolrParameter(parameterName)) {
355             LOGGER.debug("Parameter {} is a {}", parameterName, SolrParameterGroup.SolrParameter.toString());
356             return SolrParameterGroup.SolrParameter;
357         } else if (isSortParameter(parameterName)) {
358             LOGGER.debug("Parameter {} is a {}", parameterName, SolrParameterGroup.SolrParameter.toString());
359             return SolrParameterGroup.SortParameter;
360         } else {
361             LOGGER.debug("Parameter {} is a {}", parameterName, SolrParameterGroup.QueryParameter.toString());
362             return SolrParameterGroup.QueryParameter;
363         }
364     }
365 
366     /**
367      * Detects if a parameter is a solr parameter
368      *
369      * @param parameterName
370      *            the name of the parameter
371      * @return true if the parameter is a solr parameter
372      */
373     private boolean isSolrParameter(String parameterName) {
374         boolean reservedCustomKey;
375         try {
376             reservedCustomKey = MCRConfiguration2
377                 .getOrThrow(SOLR_CONFIG_PREFIX + "ReservedParameterKeys", MCRConfiguration2::splitValue)
378                 .filter(parameterName::equals)
379                 .findAny()
380                 .isPresent();
381         } catch (MCRConfigurationException e) {
382             reservedCustomKey = false;
383         }
384         return parameterName.startsWith("XSL.") || RESERVED_PARAMETER_KEYS.contains(parameterName) || reservedCustomKey;
385     }
386 
387     /**
388      * Detects if a parameter is a sort parameter
389      *
390      * @param parameterName
391      *            the name of the parameter
392      * @return true if the parameter is a sort parameter
393      */
394     private boolean isSortParameter(String parameterName) {
395         return parameterName.startsWith("sort.");
396     }
397 
398     /**
399      * Detects if a parameter is a type parameter
400      *
401      * @param parameterName
402      *            the name of the parameter
403      * @return true if the parameter is a type parameter
404      */
405     private boolean isTypeParameter(String parameterName) {
406         return parameterName.startsWith("solr.type.");
407     }
408 }