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;
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 }