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.helper;
17  
18  import java.util.ArrayList;
19  import java.util.Collections;
20  import java.util.List;
21  
22  import org.apache.logging.log4j.LogManager;
23  import org.apache.logging.log4j.Logger;
24  import org.codelibs.core.lang.StringUtil;
25  import org.codelibs.core.lang.ThreadUtil;
26  import org.codelibs.fess.Constants;
27  import org.codelibs.fess.app.service.FailureUrlService;
28  import org.codelibs.fess.ds.DataStore;
29  import org.codelibs.fess.ds.DataStoreFactory;
30  import org.codelibs.fess.ds.callback.IndexUpdateCallback;
31  import org.codelibs.fess.entity.DataStoreParams;
32  import org.codelibs.fess.mylasta.direction.FessConfig;
33  import org.codelibs.fess.opensearch.client.SearchEngineClient;
34  import org.codelibs.fess.opensearch.config.exentity.DataConfig;
35  import org.codelibs.fess.util.ComponentUtil;
36  import org.opensearch.index.query.BoolQueryBuilder;
37  import org.opensearch.index.query.QueryBuilder;
38  import org.opensearch.index.query.QueryBuilders;
39  
40  /**
41   * Helper class for managing data crawling operations in Fess.
42   * This class coordinates the execution of data store crawling processes,
43   * managing multiple concurrent crawling threads and handling the indexing
44   * of crawled documents into the search engine.
45   *
46   * <p>The DataIndexHelper supports:</p>
47   * <ul>
48   *   <li>Concurrent crawling of multiple data configurations</li>
49   *   <li>Thread pool management for crawler execution</li>
50   *   <li>Session-based crawling with cleanup operations</li>
51   *   <li>Old document deletion after successful crawling</li>
52   *   <li>Crawling execution monitoring and timing</li>
53   * </ul>
54   */
55  public class DataIndexHelper {
56  
57      /** Logger instance for this class */
58      private static final Logger logger = LogManager.getLogger(DataIndexHelper.class);
59  
60      /** Parameter key for controlling deletion of old documents */
61      private static final String DELETE_OLD_DOCS = "delete_old_docs";
62  
63      /** Parameter key for controlling retention of expired documents */
64      private static final String KEEP_EXPIRES_DOCS = "keep_expires_docs";
65  
66      /**
67       * Interval in milliseconds between crawler thread executions.
68       * Used to control the rate at which new crawler threads are started
69       * and the frequency of status checks.
70       */
71      protected long crawlingExecutionInterval = Constants.DEFAULT_CRAWLING_EXECUTION_INTERVAL;
72  
73      /**
74       * Thread priority for crawler threads.
75       * Defaults to normal thread priority.
76       */
77      protected int crawlerPriority = Thread.NORM_PRIORITY;
78  
79      /**
80       * Thread-safe list of active data crawling threads.
81       * Used to track and manage all currently running crawler threads.
82       */
83      protected final List<DataCrawlingThread> dataCrawlingThreadList = Collections.synchronizedList(new ArrayList<>());
84  
85      /**
86       * Creates a new instance of DataIndexHelper.
87       * This constructor initializes the helper for managing data crawling operations,
88       * including thread pool management and session-based crawling coordination.
89       */
90      public DataIndexHelper() {
91          // Default constructor with explicit documentation
92      }
93  
94      /**
95       * Initiates crawling for all configured data stores.
96       * This method retrieves all available data configurations and
97       * starts the crawling process for each one.
98       *
99       * @param sessionId unique identifier for this crawling session
100      */
101     public void crawl(final String sessionId) {
102         final List<DataConfig> configList = ComponentUtil.getCrawlingConfigHelper().getAllDataConfigList();
103 
104         if (configList.isEmpty()) {
105             // nothing
106             if (logger.isInfoEnabled()) {
107                 logger.info("No crawling target data.");
108             }
109             return;
110         }
111 
112         doCrawl(sessionId, configList);
113     }
114 
115     /**
116      * Initiates crawling for specific data configurations.
117      * This method starts crawling only for the data configurations
118      * specified in the configIdList parameter.
119      *
120      * @param sessionId unique identifier for this crawling session
121      * @param configIdList list of data configuration IDs to crawl
122      */
123     public void crawl(final String sessionId, final List<String> configIdList) {
124         final List<DataConfig> configList = ComponentUtil.getCrawlingConfigHelper().getDataConfigListByIds(configIdList);
125 
126         if (configList.isEmpty()) {
127             // nothing
128             if (logger.isInfoEnabled()) {
129                 logger.info("No crawling target data configs.");
130             }
131             return;
132         }
133 
134         doCrawl(sessionId, configList);
135     }
136 
137     /**
138      * Performs the actual crawling operation for the provided data configurations.
139      * This method manages the creation and execution of crawler threads,
140      * monitors their progress, and handles cleanup operations.
141      *
142      * <p>The method:</p>
143      * <ul>
144      *   <li>Creates crawler threads for each data configuration</li>
145      *   <li>Manages concurrent execution based on thread count limits</li>
146      *   <li>Monitors thread completion and handles cleanup</li>
147      *   <li>Records execution timing and statistics</li>
148      * </ul>
149      *
150      * @param sessionId unique identifier for this crawling session
151      * @param configList list of data configurations to crawl
152      */
153     protected void doCrawl(final String sessionId, final List<DataConfig> configList) {
154         final int multiprocessCrawlingCount = ComponentUtil.getFessConfig().getCrawlingThreadCount();
155 
156         final SystemHelper systemHelper = ComponentUtil.getSystemHelper();
157         final long startTime = systemHelper.getCurrentTimeAsLong();
158 
159         final IndexUpdateCallback indexUpdateCallback = ComponentUtil.getComponent(IndexUpdateCallback.class);
160 
161         final List<String> sessionIdList = new ArrayList<>();
162         dataCrawlingThreadList.clear();
163         final List<String> dataCrawlingThreadStatusList = new ArrayList<>();
164         for (final DataConfig dataConfig : configList) {
165             final DataStoreParams initParamMap = new DataStoreParams();
166             final String sid = ComponentUtil.getCrawlingConfigHelper().store(sessionId, dataConfig);
167             sessionIdList.add(sid);
168 
169             initParamMap.put(Constants.SESSION_ID, sessionId);
170             initParamMap.put(Constants.CRAWLING_INFO_ID, sid);
171 
172             final DataCrawlingThread dataCrawlingThread = new DataCrawlingThread(dataConfig, indexUpdateCallback, initParamMap);
173             dataCrawlingThread.setPriority(crawlerPriority);
174             dataCrawlingThread.setName(sid);
175             dataCrawlingThread.setDaemon(true);
176 
177             dataCrawlingThreadList.add(dataCrawlingThread);
178             dataCrawlingThreadStatusList.add(Constants.READY);
179 
180         }
181 
182         int startedCrawlerNum = 0;
183         int activeCrawlerNum = 0;
184         while (startedCrawlerNum < dataCrawlingThreadList.size()) {
185             // Force to stop crawl
186             if (systemHelper.isForceStop()) {
187                 for (final DataCrawlingThread crawlerThread : dataCrawlingThreadList) {
188                     crawlerThread.stopCrawling();
189                 }
190                 break;
191             }
192 
193             if (activeCrawlerNum < multiprocessCrawlingCount) {
194                 // start crawling
195                 dataCrawlingThreadList.get(startedCrawlerNum).start();
196                 dataCrawlingThreadStatusList.set(startedCrawlerNum, Constants.RUNNING);
197                 startedCrawlerNum++;
198                 activeCrawlerNum++;
199                 ThreadUtil.sleep(crawlingExecutionInterval);
200                 continue;
201             }
202 
203             // check status
204             for (int i = 0; i < startedCrawlerNum; i++) {
205                 if (!dataCrawlingThreadList.get(i).isRunning() && Constants.RUNNING.equals(dataCrawlingThreadStatusList.get(i))) {
206                     dataCrawlingThreadList.get(i).awaitTermination();
207                     dataCrawlingThreadStatusList.set(i, Constants.DONE);
208                     activeCrawlerNum--;
209                 }
210             }
211             ThreadUtil.sleep(crawlingExecutionInterval);
212         }
213 
214         boolean finishedAll = false;
215         while (!finishedAll) {
216             finishedAll = true;
217             for (int i = 0; i < dataCrawlingThreadList.size(); i++) {
218                 dataCrawlingThreadList.get(i).awaitTermination(crawlingExecutionInterval);
219                 if (!dataCrawlingThreadList.get(i).isRunning() && Constants.RUNNING.equals(dataCrawlingThreadStatusList.get(i))) {
220                     dataCrawlingThreadStatusList.set(i, Constants.DONE);
221                 }
222                 if (!Constants.DONE.equals(dataCrawlingThreadStatusList.get(i))) {
223                     finishedAll = false;
224                 }
225             }
226         }
227         dataCrawlingThreadList.clear();
228         dataCrawlingThreadStatusList.clear();
229 
230         // put cralwing info
231         final CrawlingInfoHelper crawlingInfoHelper = ComponentUtil.getCrawlingInfoHelper();
232 
233         final long execTime = systemHelper.getCurrentTimeAsLong() - startTime;
234         crawlingInfoHelper.putToInfoMap(Constants.DATA_CRAWLING_EXEC_TIME, Long.toString(execTime));
235         if (logger.isInfoEnabled()) {
236             logger.info("[EXEC TIME] crawling time: {}ms", execTime);
237         }
238 
239         crawlingInfoHelper.putToInfoMap(Constants.DATA_INDEX_EXEC_TIME, Long.toString(indexUpdateCallback.getExecuteTime()));
240         crawlingInfoHelper.putToInfoMap(Constants.DATA_INDEX_SIZE, Long.toString(indexUpdateCallback.getDocumentSize()));
241 
242         for (final String sid : sessionIdList) {
243             // remove config
244             ComponentUtil.getCrawlingConfigHelper().remove(sid);
245         }
246 
247     }
248 
249     /**
250      * Inner thread class for executing data store crawling operations.
251      * Each thread handles crawling for a single data configuration,
252      * processing documents and updating the search index.
253      *
254      * <p>The thread manages:</p>
255      * <ul>
256      *   <li>Data store initialization and document processing</li>
257      *   <li>Index update operations through callbacks</li>
258      *   <li>Error handling and failure logging</li>
259      *   <li>Cleanup of old documents after successful crawling</li>
260      * </ul>
261      */
262     protected static class DataCrawlingThread extends Thread {
263 
264         /** Configuration for the data store being crawled */
265         private final DataConfig dataConfig;
266 
267         /** Callback for handling document indexing operations */
268         private final IndexUpdateCallback indexUpdateCallback;
269 
270         /** Initialization parameters for the data store */
271         private final DataStoreParams initParamMap;
272 
273         /** Flag indicating whether the crawling thread has finished execution */
274         protected boolean finished = false;
275 
276         /** Flag indicating whether the crawling thread is currently running */
277         protected boolean running = false;
278 
279         /** The data store instance used for crawling operations */
280         private DataStore dataStore;
281 
282         /**
283          * Constructs a new data crawling thread.
284          *
285          * @param dataConfig configuration for the data store to crawl
286          * @param indexUpdateCallback callback for handling document indexing
287          * @param initParamMap initialization parameters for the data store
288          */
289         protected DataCrawlingThread(final DataConfig dataConfig, final IndexUpdateCallback indexUpdateCallback,
290                 final DataStoreParams initParamMap) {
291             this.dataConfig = dataConfig;
292             this.indexUpdateCallback = indexUpdateCallback;
293             this.initParamMap = initParamMap;
294         }
295 
296         /**
297          * Executes the crawling thread.
298          * Sets the running flag, processes the data store, and ensures
299          * proper cleanup regardless of success or failure.
300          */
301         @Override
302         public void run() {
303             running = true;
304             try {
305                 process();
306             } finally {
307                 running = false;
308                 finished = true;
309             }
310         }
311 
312         /**
313          * Processes the data store crawling operation.
314          * This method initializes the data store, performs the crawling,
315          * handles any errors, and ensures cleanup operations are executed.
316          * After successful crawling, it commits the index updates and
317          * deletes old documents if configured to do so.
318          */
319         protected void process() {
320             final DataStoreFactory dataStoreFactory = ComponentUtil.getDataStoreFactory();
321             dataStore = dataStoreFactory.getDataStore(dataConfig.getHandlerName());
322             if (dataStore == null) {
323                 logger.error("DataStore({}) is not found.", dataConfig.getHandlerName());
324             } else {
325                 try {
326                     dataStore.store(dataConfig, indexUpdateCallback, initParamMap);
327                 } catch (final Throwable e) {
328                     logger.error("Failed to process a data crawling: {}", dataConfig.getName(), e);
329                     ComponentUtil.getComponent(FailureUrlService.class)
330                             .store(dataConfig, e.getClass().getCanonicalName(), dataConfig.getConfigId() + ":" + dataConfig.getName(), e);
331                 } finally {
332                     indexUpdateCallback.commit();
333                     deleteOldDocs();
334                 }
335             }
336         }
337 
338         /**
339          * Deletes old documents from the search index.
340          * This method removes documents that were indexed in previous
341          * crawling sessions for the same data configuration, keeping
342          * only the documents from the current session.
343          *
344          * <p>The deletion process:</p>
345          * <ul>
346          *   <li>Checks if old document deletion is enabled</li>
347          *   <li>Builds a query to find old documents for this configuration</li>
348          *   <li>Optionally preserves expired documents based on configuration</li>
349          *   <li>Executes the deletion query against the search engine</li>
350          * </ul>
351          */
352         private void deleteOldDocs() {
353             if (Constants.FALSE.equals(initParamMap.getAsString(DELETE_OLD_DOCS))) {
354                 return;
355             }
356             final String sessionId = initParamMap.getAsString(Constants.SESSION_ID);
357             if (StringUtil.isBlank(sessionId)) {
358                 logger.warn("[{}] Cannot delete stale documents: sessionId is not set.", dataConfig.getName());
359                 return;
360             }
361             final FessConfig fessConfig = ComponentUtil.getFessConfig();
362             final BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery()//
363                     .must(QueryBuilders.termQuery(fessConfig.getIndexFieldConfigId(), dataConfig.getConfigId()))//
364                     .mustNot(QueryBuilders.termQuery(fessConfig.getIndexFieldSegment(), sessionId));
365             if (!Constants.FALSE.equals(initParamMap.getAsString(KEEP_EXPIRES_DOCS))) {
366                 final QueryBuilder expiresCheckQuery = QueryBuilders.boolQuery()//
367                         .mustNot(QueryBuilders.rangeQuery(fessConfig.getIndexFieldExpires()).gt("now"))//
368                         .mustNot(QueryBuilders.existsQuery(fessConfig.getIndexFieldExpires()));
369                 queryBuilder.must(expiresCheckQuery);
370             }
371 
372             try {
373                 final SearchEngineClient searchEngineClient = ComponentUtil.getSearchEngineClient();
374                 final String index = fessConfig.getIndexDocumentUpdateIndex();
375                 searchEngineClient.admin().indices().prepareRefresh(index).execute().actionGet();
376                 final long numOfDeleted = searchEngineClient.deleteByQuery(index, queryBuilder);
377                 logger.info("[{}] Deleted {} stale documents.", dataConfig.getName(), numOfDeleted);
378             } catch (final Exception e) {
379                 logger.error("[{}] Failed to delete stale documents.", dataConfig.getName(), e);
380             }
381         }
382 
383         /**
384          * Checks if the crawling thread has finished execution.
385          *
386          * @return true if the thread has completed its crawling operation
387          */
388         public boolean isFinished() {
389             return finished;
390         }
391 
392         /**
393          * Stops the crawling operation gracefully.
394          * If a data store is currently active, this method calls
395          * its stop method to halt the crawling process.
396          */
397         public void stopCrawling() {
398             if (dataStore != null) {
399                 dataStore.stop();
400             }
401         }
402 
403         /**
404          * Gets the crawling information ID for this thread.
405          *
406          * @return the crawling info ID from the initialization parameters
407          */
408         public String getCrawlingInfoId() {
409             return initParamMap.getAsString(Constants.CRAWLING_INFO_ID);
410         }
411 
412         /**
413          * Checks if the crawling thread is currently running.
414          *
415          * @return true if the thread is actively executing
416          */
417         public boolean isRunning() {
418             return running;
419         }
420 
421         /**
422          * Waits for the crawling thread to terminate.
423          * This method blocks until the thread completes its execution.
424          * Interrupted exceptions are caught and logged at debug level.
425          */
426         public void awaitTermination() {
427             try {
428                 join();
429             } catch (final InterruptedException e) {
430                 if (logger.isDebugEnabled()) {
431                     logger.debug("Interrupted.", e);
432                 }
433             }
434         }
435 
436         /**
437          * Waits for the crawling thread to terminate within a specified time limit.
438          * This method blocks until the thread completes or the timeout expires.
439          *
440          * @param mills maximum time to wait in milliseconds
441          */
442         public void awaitTermination(final long mills) {
443             try {
444                 join(mills);
445             } catch (final InterruptedException e) {
446                 if (logger.isDebugEnabled()) {
447                     logger.debug("Interrupted.", e);
448                 }
449             }
450         }
451     }
452 
453     /**
454      * Sets the crawling execution interval.
455      * This interval controls the delay between starting new crawler threads
456      * and the frequency of status checks during crawling operations.
457      *
458      * @param crawlingExecutionInterval interval in milliseconds
459      */
460     public void setCrawlingExecutionInterval(final long crawlingExecutionInterval) {
461         this.crawlingExecutionInterval = crawlingExecutionInterval;
462     }
463 
464     /**
465      * Sets the thread priority for crawler threads.
466      * This priority will be applied to all newly created crawler threads.
467      *
468      * @param crawlerPriority thread priority (typically Thread.MIN_PRIORITY to Thread.MAX_PRIORITY)
469      */
470     public void setCrawlerPriority(final int crawlerPriority) {
471         this.crawlerPriority = crawlerPriority;
472     }
473 }