View Javadoc
1   /*
2    * Copyright 2012-2025 CodeLibs Project and the Others.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13   * either express or implied. See the License for the specific language
14   * governing permissions and limitations under the License.
15   */
16  package org.codelibs.fess.query.parser;
17  
18  import java.util.ArrayList;
19  import java.util.List;
20  
21  import org.apache.lucene.analysis.Analyzer;
22  import org.apache.lucene.analysis.core.WhitespaceAnalyzer;
23  import org.apache.lucene.queryparser.classic.ParseException;
24  import org.apache.lucene.queryparser.classic.QueryParser.Operator;
25  import org.apache.lucene.queryparser.ext.ExtendableQueryParser;
26  import org.apache.lucene.queryparser.ext.Extensions.Pair;
27  import org.apache.lucene.search.PhraseQuery;
28  import org.apache.lucene.search.Query;
29  import org.apache.lucene.search.TermQuery;
30  import org.codelibs.fess.Constants;
31  import org.codelibs.fess.exception.QueryParseException;
32  import org.lastaflute.web.util.LaRequestUtil;
33  
34  import jakarta.annotation.PostConstruct;
35  
36  /**
37   * A query parser that processes search queries and converts them to Lucene Query objects.
38   * This class provides a flexible architecture using a chain of filters to process and transform
39   * queries before they are parsed by the underlying Lucene query parser.
40   *
41   * <p>The parser supports configuration of default field, analyzer, wildcard settings,
42   * and default operator. It also allows adding custom filters to modify query behavior.</p>
43   *
44   */
45  public class QueryParser {
46  
47      /**
48       * Default constructor.
49       */
50      public QueryParser() {
51          // Default constructor
52      }
53  
54      /** The default field to search in when no field is specified in the query */
55      protected String defaultField = Constants.DEFAULT_FIELD;
56  
57      /** The analyzer used to analyze query terms */
58      protected Analyzer analyzer = new WhitespaceAnalyzer();
59  
60      /** Whether to allow leading wildcards in query terms */
61      protected boolean allowLeadingWildcard = true;
62  
63      /** The default operator to use between query terms */
64      protected Operator defaultOperator = Operator.AND;
65  
66      /** List of filters to apply to queries */
67      protected List<Filter> filterList = new ArrayList<>();
68  
69      /** The filter chain used to process queries */
70      protected FilterChain filterChain;
71  
72      /**
73       * Initializes the query parser by creating the filter chain.
74       * This method is called automatically after construction.
75       */
76      @PostConstruct
77      public void init() {
78          createFilterChain();
79      }
80  
81      /**
82       * Parses the given query string and returns a Lucene Query object.
83       * The query is processed through the filter chain before being parsed.
84       *
85       * @param query the query string to parse
86       * @return the parsed Query object
87       * @throws QueryParseException if the query cannot be parsed
88       */
89      public Query parse(final String query) {
90          return filterChain.parse(query);
91      }
92  
93      /**
94       * Creates a new Lucene query parser with the current configuration.
95       * The parser is configured with the default field, analyzer, wildcard settings,
96       * and default operator.
97       *
98       * @return a configured Lucene query parser
99       */
100     protected org.apache.lucene.queryparser.classic.QueryParser createQueryParser() {
101         final LuceneQueryParser parser = new LuceneQueryParser(defaultField, analyzer);
102         parser.setAllowLeadingWildcard(allowLeadingWildcard);
103         LaRequestUtil.getOptionalRequest().ifPresent(req -> {
104             if (req.getAttribute(Constants.DEFAULT_QUERY_OPERATOR) instanceof final String op) {
105                 parser.setDefaultOperator(Operator.valueOf(op));
106             } else {
107                 parser.setDefaultOperator(defaultOperator);
108             }
109         }).orElse(() -> {
110             parser.setDefaultOperator(defaultOperator);
111         });
112         return parser;
113     }
114 
115     /**
116      * Sets the default field to search in when no field is specified in the query.
117      *
118      * @param defaultField the default field name
119      */
120     public void setDefaultField(final String defaultField) {
121         this.defaultField = defaultField;
122     }
123 
124     /**
125      * Sets the analyzer used to analyze query terms.
126      *
127      * @param analyzer the analyzer to use
128      */
129     public void setAnalyzer(final Analyzer analyzer) {
130         this.analyzer = analyzer;
131     }
132 
133     /**
134      * Sets whether to allow leading wildcards in query terms.
135      *
136      * @param allowLeadingWildcard true to allow leading wildcards, false otherwise
137      */
138     public void setAllowLeadingWildcard(final boolean allowLeadingWildcard) {
139         this.allowLeadingWildcard = allowLeadingWildcard;
140     }
141 
142     /**
143      * Sets the default operator to use between query terms.
144      *
145      * @param defaultOperator the default operator (AND or OR)
146      */
147     public void setDefaultOperator(final Operator defaultOperator) {
148         this.defaultOperator = defaultOperator;
149     }
150 
151     /**
152      * Adds a filter to the query processing chain.
153      * The filter chain is recreated after adding the filter.
154      *
155      * @param filter the filter to add
156      */
157     public void addFilter(final Filter filter) {
158         filterList.add(filter);
159         createFilterChain();
160     }
161 
162     /**
163      * Creates the filter chain by combining all registered filters.
164      * The chain starts with the default filter chain and appends each registered filter.
165      */
166     protected void createFilterChain() {
167         FilterChain chain = createDefaultFilterChain();
168         for (final Filter element : filterList) {
169             chain = appendFilterChain(element, chain);
170         }
171         filterChain = chain;
172     }
173 
174     /**
175      * Appends a filter to the existing filter chain.
176      *
177      * @param filter the filter to append
178      * @param chain the existing filter chain
179      * @return a new filter chain with the filter appended
180      */
181     protected FilterChain appendFilterChain(final Filter filter, final FilterChain chain) {
182         return query -> filter.parse(query, chain);
183     }
184 
185     /**
186      * Creates the default filter chain that performs the actual query parsing.
187      * This chain uses the Lucene query parser to parse the query string.
188      *
189      * @return the default filter chain
190      */
191     protected FilterChain createDefaultFilterChain() {
192         return query -> {
193             try {
194                 return createQueryParser().parse(query);
195             } catch (final ParseException e) {
196                 throw new QueryParseException(e);
197             }
198         };
199     }
200 
201     /**
202      * Interface for query filters that can modify or transform queries.
203      * Filters are applied in the order they are added to the parser.
204      */
205     public interface Filter {
206         /**
207          * Parses and potentially modifies the query string.
208          *
209          * @param query the query string to process
210          * @param chain the next filter chain to invoke
211          * @return the processed Query object
212          */
213         Query parse(final String query, final FilterChain chain);
214     }
215 
216     /**
217      * Interface for the filter chain that processes queries.
218      * Each filter in the chain can invoke the next filter or terminate the chain.
219      */
220     public interface FilterChain {
221         /**
222          * Parses the query string and returns a Query object.
223          *
224          * @param query the query string to parse
225          * @return the parsed Query object
226          */
227         Query parse(final String query);
228     }
229 
230     /**
231      * Custom Lucene query parser that extends the standard QueryParser
232      * to provide additional functionality for quoted queries.
233      */
234     protected static class LuceneQueryParser extends org.apache.lucene.queryparser.classic.QueryParser {
235 
236         /** The default field for queries */
237         private final String defaultField;
238 
239         /**
240          * Creates a new {@link ExtendableQueryParser} instance
241          *
242          * @param f the default query field
243          * @param a the analyzer used to find terms in a query string
244          */
245         public LuceneQueryParser(final String f, final Analyzer a) {
246             super(f, a);
247             defaultField = f;
248         }
249 
250         /**
251          * Overrides the field query creation to handle quoted queries specially.
252          * For quoted queries on the default field, creates a phrase query instead of a term query.
253          *
254          * @param field the field to query
255          * @param queryText the query text
256          * @param quoted whether the query is quoted
257          * @return the created Query object
258          * @throws ParseException if the query cannot be parsed
259          */
260         @Override
261         protected Query getFieldQuery(final String field, final String queryText, final boolean quoted) throws ParseException {
262             final org.apache.lucene.search.Query query = super.getFieldQuery(field, queryText, quoted);
263             if (quoted && query instanceof final TermQuery termQuery) {
264                 final Pair<String, String> splitField = splitField(defaultField, field);
265                 if (defaultField.equals(splitField.cud())) {
266                     final PhraseQuery.Builder builder = new PhraseQuery.Builder();
267                     builder.add(termQuery.getTerm());
268                     return builder.build();
269                 }
270             }
271             return query;
272         }
273 
274         /**
275          * Splits a field name into its components.
276          *
277          * @param defaultField the default field name
278          * @param field the field name to split
279          * @return a Pair containing the field name and extension key
280          */
281         protected Pair<String, String> splitField(final String defaultField, final String field) {
282             final int indexOf = field.indexOf(':');
283             if (indexOf < 0) {
284                 return new Pair<>(field, null);
285             }
286             final String indexField = indexOf == 0 ? defaultField : field.substring(0, indexOf);
287             final String extensionKey = field.substring(indexOf + 1);
288             return new Pair<>(indexField, extensionKey);
289         }
290     }
291 }