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 }