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;
17
18 import java.util.ArrayList;
19 import java.util.HashMap;
20 import java.util.List;
21 import java.util.Map;
22
23 import org.apache.logging.log4j.LogManager;
24 import org.apache.logging.log4j.Logger;
25 import org.apache.lucene.search.Query;
26 import org.codelibs.fess.entity.QueryContext;
27 import org.codelibs.fess.exception.InvalidQueryException;
28 import org.lastaflute.core.message.UserMessages;
29 import org.opensearch.index.query.QueryBuilder;
30
31 import jakarta.annotation.PostConstruct;
32
33 /**
34 * Query processor component that handles query filters and commands.
35 * This class provides a pipeline for processing Lucene queries by applying
36 * a chain of filters and executing registered query commands.
37 *
38 * <p>The processor maintains a map of query commands indexed by query class names
39 * and a list of filters that are applied in order during query processing.</p>
40 */
41 public class QueryProcessor {
42
43 /**
44 * Default constructor.
45 */
46 public QueryProcessor() {
47 // Default constructor
48 }
49
50 private static final Logger logger = LogManager.getLogger(QueryProcessor.class);
51
52 /**
53 * Map of query commands indexed by query class simple names.
54 * Used to lookup appropriate command handlers for different query types.
55 */
56 protected Map<String, QueryCommand> queryCommandMap = new HashMap<>();
57
58 /**
59 * List of filters that will be applied during query processing.
60 * Filters are applied in the order they are added to this list.
61 */
62 protected List<Filter> filterList = new ArrayList<>();
63
64 /**
65 * The filter chain that processes queries through all registered filters
66 * before executing the appropriate query command.
67 */
68 protected FilterChain filterChain;
69
70 /**
71 * Initializes the query processor after construction.
72 * This method creates the initial filter chain from the registered filters.
73 * Called automatically by the DI container after bean construction.
74 */
75 @PostConstruct
76 public void init() {
77 createFilterChain();
78 }
79
80 /**
81 * Executes query processing through the filter chain.
82 *
83 * @param context the query context containing search parameters and state
84 * @param query the Lucene query to be processed
85 * @param boost the boost factor to apply to the query
86 * @return the processed OpenSearch QueryBuilder
87 */
88 public QueryBuilder execute(final QueryContext context, final Query query, final float boost) {
89 return filterChain.execute(context, query, boost);
90 }
91
92 /**
93 * Adds a query command to the processor.
94 *
95 * @param name the name to associate with the command (typically the query class simple name)
96 * @param queryCommand the query command implementation to add
97 * @throws IllegalArgumentException if name or queryCommand is null
98 */
99 public void add(final String name, final QueryCommand queryCommand) {
100 if (name == null || queryCommand == null) {
101 throw new IllegalArgumentException(
102 "Both name and queryCommand parameters are required. name: " + name + ", queryCommand: " + queryCommand);
103 }
104 if (logger.isDebugEnabled()) {
105 logger.debug("Loaded QueryCommand: {}", name);
106 }
107 queryCommandMap.put(name, queryCommand);
108 }
109
110 /**
111 * Adds a filter to the processing pipeline.
112 * After adding a filter, the filter chain is recreated to include the new filter.
113 *
114 * @param filter the filter to add to the processing pipeline
115 */
116 public void addFilter(final Filter filter) {
117 filterList.add(filter);
118 createFilterChain();
119 }
120
121 /**
122 * Creates the filter chain from the registered filters.
123 * The chain starts with the default filter chain and appends each registered filter.
124 */
125 protected void createFilterChain() {
126 FilterChain chain = createDefaultFilterChain();
127 for (final Filter element : filterList) {
128 chain = appendFilterChain(element, chain);
129 }
130 filterChain = chain;
131 }
132
133 /**
134 * Appends a filter to an existing filter chain.
135 *
136 * @param filter the filter to append
137 * @param chain the existing filter chain to append to
138 * @return a new filter chain that includes the appended filter
139 */
140 protected FilterChain appendFilterChain(final Filter filter, final FilterChain chain) {
141 return (context, query, boost) -> filter.execute(context, query, boost, chain);
142 }
143
144 /**
145 * Creates the default filter chain that executes query commands.
146 * This chain looks up the appropriate query command based on the query class name
147 * and executes it. If no command is found, throws an InvalidQueryException.
148 *
149 * @return the default filter chain implementation
150 */
151 protected FilterChain createDefaultFilterChain() {
152 return (context, query, boost) -> {
153 final QueryCommand queryCommand = queryCommandMap.get(query.getClass().getSimpleName());
154 if (queryCommand != null) {
155 return queryCommand.execute(context, query, boost);
156 }
157 throw new InvalidQueryException(messages -> messages.addErrorsInvalidQueryUnknown(UserMessages.GLOBAL_PROPERTY_KEY),
158 "Unknown q: " + query.getClass() + " => " + query);
159 };
160 }
161
162 /**
163 * Interface for query processing filters.
164 * Filters can modify, validate, or enhance queries before they are executed.
165 */
166 public interface Filter {
167 /**
168 * Executes the filter logic on the given query.
169 *
170 * @param context the query context containing search parameters and state
171 * @param query the Lucene query to be processed
172 * @param boost the boost factor to apply to the query
173 * @param chain the next filter chain to execute after this filter
174 * @return the processed OpenSearch QueryBuilder
175 */
176 QueryBuilder execute(final QueryContext context, final Query query, final float boost, final FilterChain chain);
177 }
178
179 /**
180 * Interface for filter chains that process queries through a sequence of filters.
181 * This follows the Chain of Responsibility pattern for query processing.
182 */
183 public interface FilterChain {
184 /**
185 * Executes the filter chain on the given query.
186 *
187 * @param context the query context containing search parameters and state
188 * @param query the Lucene query to be processed
189 * @param boost the boost factor to apply to the query
190 * @return the processed OpenSearch QueryBuilder
191 */
192 QueryBuilder execute(final QueryContext context, final Query query, final float boost);
193 }
194 }