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  import java.util.Map;
22  import java.util.concurrent.ConcurrentHashMap;
23  import java.util.concurrent.TimeUnit;
24  import java.util.regex.Pattern;
25  
26  import org.apache.logging.log4j.LogManager;
27  import org.apache.logging.log4j.Logger;
28  import org.codelibs.core.lang.StringUtil;
29  import org.codelibs.fess.Constants;
30  import org.codelibs.fess.app.service.DataConfigService;
31  import org.codelibs.fess.app.service.FileConfigService;
32  import org.codelibs.fess.app.service.WebConfigService;
33  import org.codelibs.fess.mylasta.direction.FessConfig;
34  import org.codelibs.fess.opensearch.config.exbhv.DataConfigBhv;
35  import org.codelibs.fess.opensearch.config.exbhv.FailureUrlBhv;
36  import org.codelibs.fess.opensearch.config.exbhv.FileConfigBhv;
37  import org.codelibs.fess.opensearch.config.exbhv.WebConfigBhv;
38  import org.codelibs.fess.opensearch.config.exentity.CrawlingConfig;
39  import org.codelibs.fess.opensearch.config.exentity.CrawlingConfig.ConfigName;
40  import org.codelibs.fess.opensearch.config.exentity.CrawlingConfig.ConfigType;
41  import org.codelibs.fess.opensearch.config.exentity.CrawlingConfig.Param.Config;
42  import org.codelibs.fess.opensearch.config.exentity.DataConfig;
43  import org.codelibs.fess.opensearch.config.exentity.FailureUrl;
44  import org.codelibs.fess.opensearch.config.exentity.FileConfig;
45  import org.codelibs.fess.opensearch.config.exentity.WebConfig;
46  import org.codelibs.fess.util.ComponentUtil;
47  import org.dbflute.cbean.result.ListResultBean;
48  import org.dbflute.optional.OptionalEntity;
49  import org.dbflute.optional.OptionalThing;
50  
51  import com.google.common.cache.Cache;
52  import com.google.common.cache.CacheBuilder;
53  
54  import jakarta.annotation.PostConstruct;
55  
56  /**
57   * Helper class for managing crawling configurations.
58   * Provides functionality to store, retrieve, and manage different types of crawling configurations
59   * including web, file, and data configurations. Supports caching and session-based configuration management.
60   */
61  public class CrawlingConfigHelper {
62  
63      /**
64       * Creates a new instance of CrawlingConfigHelper.
65       */
66      public CrawlingConfigHelper() {
67          // Default constructor
68      }
69  
70      private static final Logger logger = LogManager.getLogger(CrawlingConfigHelper.class);
71  
72      /**
73       * Map storing crawling configurations by session ID.
74       */
75      protected final Map<String, CrawlingConfig> crawlingConfigMap = new ConcurrentHashMap<>();
76  
77      /**
78       * Counter for generating unique session identifiers.
79       */
80      protected int count = 1;
81  
82      /**
83       * Cache for storing crawling configurations to improve performance.
84       */
85      protected Cache<String, CrawlingConfig> crawlingConfigCache;
86  
87      /**
88       * Initializes the CrawlingConfigHelper by setting up the crawling configuration cache.
89       * This method is called automatically after the bean construction is complete.
90       * The cache is configured with a maximum size of 100 entries and expires after 10 minutes.
91       */
92      @PostConstruct
93      public void init() {
94          if (logger.isDebugEnabled()) {
95              logger.debug("Initializing {}", this.getClass().getSimpleName());
96          }
97          crawlingConfigCache = CacheBuilder.newBuilder().maximumSize(100).expireAfterWrite(10, TimeUnit.MINUTES).build();
98      }
99  
100     /**
101      * Determines the configuration type from a given config ID.
102      * The config type is identified by the first character of the config ID.
103      *
104      * @param configId the configuration ID to analyze
105      * @return the ConfigType (WEB, FILE, or DATA) or null if the config ID is invalid or doesn't match any known type
106      */
107     public ConfigType getConfigType(final String configId) {
108         if (configId == null || configId.length() < 2) {
109             return null;
110         }
111         final String configType = configId.substring(0, 1);
112         if (ConfigType.WEB.getTypePrefix().equals(configType)) {
113             return ConfigType.WEB;
114         }
115         if (ConfigType.FILE.getTypePrefix().equals(configType)) {
116             return ConfigType.FILE;
117         }
118         if (ConfigType.DATA.getTypePrefix().equals(configType)) {
119             return ConfigType.DATA;
120         }
121         return null;
122     }
123 
124     /**
125      * Extracts the actual ID from a config ID by removing the type prefix.
126      * Config IDs are formatted as [type_prefix][actual_id], so this method
127      * returns everything after the first character.
128      *
129      * @param configId the configuration ID to process
130      * @return the actual ID without the type prefix, or null if the config ID is invalid
131      */
132     protected String getId(final String configId) {
133         if (configId == null || configId.length() < 2) {
134             return null;
135         }
136         return configId.substring(1);
137     }
138 
139     /**
140      * Retrieves a crawling configuration by its config ID.
141      * This method uses caching to improve performance and automatically determines
142      * the configuration type and delegates to the appropriate service.
143      *
144      * @param configId the configuration ID to retrieve
145      * @return the CrawlingConfig object or null if not found or on error
146      */
147     public CrawlingConfig getCrawlingConfig(final String configId) {
148         try {
149             return crawlingConfigCache.get(configId, () -> {
150                 final ConfigType configType = getConfigType(configId);
151                 if (configType == null) {
152                     return null;
153                 }
154                 final String id = getId(configId);
155                 if (id == null) {
156                     return null;
157                 }
158                 return switch (configType) {
159                 case WEB -> {
160                     final WebConfigService webConfigService = ComponentUtil.getComponent(WebConfigService.class);
161                     yield webConfigService.getWebConfig(id).get();
162                 }
163                 case FILE -> {
164                     final FileConfigService fileConfigService = ComponentUtil.getComponent(FileConfigService.class);
165                     yield fileConfigService.getFileConfig(id).get();
166                 }
167                 case DATA -> {
168                     final DataConfigService dataConfigService = ComponentUtil.getComponent(DataConfigService.class);
169                     yield dataConfigService.getDataConfig(id).get();
170                 }
171                 default -> null;
172                 };
173             });
174         } catch (final Exception e) {
175             logger.warn("Failed to access a crawling config cache: {}", configId, e);
176             return null;
177         }
178     }
179 
180     /**
181      * Retrieves the pipeline configuration parameter for a given config ID.
182      * The pipeline parameter is extracted from the crawling configuration's parameter map.
183      *
184      * @param configId the configuration ID to get the pipeline for
185      * @return an OptionalThing containing the pipeline string if found, or empty if not found or blank
186      */
187     public OptionalThing<String> getPipeline(final String configId) {
188         final CrawlingConfig config = getCrawlingConfig(configId);
189         if (config == null) {
190             return OptionalThing.empty();
191         }
192         final String pipeline = config.getConfigParameterMap(ConfigName.CONFIG).get(Config.PIPELINE);
193         if (StringUtil.isBlank(pipeline)) {
194             return OptionalThing.empty();
195         }
196         return OptionalThing.of(pipeline);
197     }
198 
199     /**
200      * Refreshes the crawling configuration cache by invalidating all cached entries.
201      * This forces the next access to reload configurations from the underlying services.
202      */
203     public void refresh() {
204         crawlingConfigCache.invalidateAll();
205     }
206 
207     /**
208      * Stores a crawling configuration in the session-based storage with a unique identifier.
209      * The generated session count ID combines the session ID with an incrementing counter.
210      *
211      * @param sessionId the session identifier
212      * @param crawlingConfig the crawling configuration to store
213      * @return the unique session count ID that can be used to retrieve the stored configuration
214      */
215     public synchronized String store(final String sessionId, final CrawlingConfig crawlingConfig) {
216         final String sessionCountId = sessionId + "-" + count;
217         crawlingConfigMap.put(sessionCountId, crawlingConfig);
218         count++;
219         return sessionCountId;
220     }
221 
222     /**
223      * Removes a stored crawling configuration from the session-based storage.
224      *
225      * @param sessionCountId the session count ID of the configuration to remove
226      */
227     public void remove(final String sessionCountId) {
228         crawlingConfigMap.remove(sessionCountId);
229     }
230 
231     /**
232      * Retrieves a stored crawling configuration from the session-based storage.
233      *
234      * @param sessionCountId the session count ID of the configuration to retrieve
235      * @return the stored CrawlingConfig or null if not found
236      */
237     public CrawlingConfig get(final String sessionCountId) {
238         return crawlingConfigMap.get(sessionCountId);
239     }
240 
241     /**
242      * Retrieves all available web crawling configurations.
243      * This is a convenience method that calls the overloaded version with default parameters
244      * (withLabelType=true, withRoleType=true, available=true, idList=null).
245      *
246      * @return a list of all available WebConfig objects
247      */
248     public List<WebConfig> getAllWebConfigList() {
249         return getAllWebConfigList(true, true, true, null);
250     }
251 
252     /**
253      * Retrieves web crawling configurations filtered by a list of IDs.
254      * If the ID list is null, returns all available configurations.
255      *
256      * @param idList the list of configuration IDs to retrieve, or null for all configurations
257      * @return a list of WebConfig objects with the specified IDs
258      */
259     public List<WebConfig> getWebConfigListByIds(final List<String> idList) {
260         if (idList == null) {
261             return getAllWebConfigList();
262         }
263         return getAllWebConfigList(true, true, false, idList);
264     }
265 
266     /**
267      * Retrieves web crawling configurations with various filtering options.
268      *
269      * @param withLabelType whether to include label type information (currently not used in implementation)
270      * @param withRoleType whether to include role type information (currently not used in implementation)
271      * @param available whether to filter only available configurations
272      * @param idList the list of configuration IDs to retrieve, or null for no ID filtering
273      * @return a list of WebConfig objects matching the criteria
274      */
275     public List<WebConfig> getAllWebConfigList(final boolean withLabelType, final boolean withRoleType, final boolean available,
276             final List<String> idList) {
277         return ComponentUtil.getComponent(WebConfigBhv.class).selectList(cb -> {
278             if (available) {
279                 cb.query().setAvailable_Equal(Constants.T);
280             }
281             if (idList != null) {
282                 cb.query().setId_InScope(idList);
283             }
284             cb.query().setName_NotEqual(ComponentUtil.getFessConfig().getFormAdminDefaultTemplateName());
285             cb.query().addOrderBy_SortOrder_Asc();
286             cb.query().addOrderBy_Name_Asc();
287             cb.fetchFirst(ComponentUtil.getFessConfig().getPageWebConfigMaxFetchSizeAsInteger());
288         });
289     }
290 
291     /**
292      * Retrieves all available file crawling configurations.
293      * This is a convenience method that calls the overloaded version with default parameters
294      * (withLabelType=true, withRoleType=true, available=true, idList=null).
295      *
296      * @return a list of all available FileConfig objects
297      */
298     public List<FileConfig> getAllFileConfigList() {
299         return getAllFileConfigList(true, true, true, null);
300     }
301 
302     /**
303      * Retrieves file crawling configurations filtered by a list of IDs.
304      * If the ID list is null, returns all available configurations.
305      *
306      * @param idList the list of configuration IDs to retrieve, or null for all configurations
307      * @return a list of FileConfig objects with the specified IDs
308      */
309     public List<FileConfig> getFileConfigListByIds(final List<String> idList) {
310         if (idList == null) {
311             return getAllFileConfigList();
312         }
313         return getAllFileConfigList(true, true, false, idList);
314     }
315 
316     /**
317      * Retrieves file crawling configurations with various filtering options.
318      *
319      * @param withLabelType whether to include label type information (currently not used in implementation)
320      * @param withRoleType whether to include role type information (currently not used in implementation)
321      * @param available whether to filter only available configurations
322      * @param idList the list of configuration IDs to retrieve, or null for no ID filtering
323      * @return a list of FileConfig objects matching the criteria
324      */
325     public List<FileConfig> getAllFileConfigList(final boolean withLabelType, final boolean withRoleType, final boolean available,
326             final List<String> idList) {
327         return ComponentUtil.getComponent(FileConfigBhv.class).selectList(cb -> {
328             if (available) {
329                 cb.query().setAvailable_Equal(Constants.T);
330             }
331             if (idList != null) {
332                 cb.query().setId_InScope(idList);
333             }
334             cb.query().setName_NotEqual(ComponentUtil.getFessConfig().getFormAdminDefaultTemplateName());
335             cb.query().addOrderBy_SortOrder_Asc();
336             cb.query().addOrderBy_Name_Asc();
337             cb.fetchFirst(ComponentUtil.getFessConfig().getPageFileConfigMaxFetchSizeAsInteger());
338         });
339     }
340 
341     /**
342      * Retrieves all available data crawling configurations.
343      * This is a convenience method that calls the overloaded version with default parameters
344      * (withLabelType=true, withRoleType=true, available=true, idList=null).
345      *
346      * @return a list of all available DataConfig objects
347      */
348     public List<DataConfig> getAllDataConfigList() {
349         return getAllDataConfigList(true, true, true, null);
350     }
351 
352     /**
353      * Retrieves data crawling configurations filtered by a list of IDs.
354      * If the ID list is null, returns all available configurations.
355      *
356      * @param idList the list of configuration IDs to retrieve, or null for all configurations
357      * @return a list of DataConfig objects with the specified IDs
358      */
359     public List<DataConfig> getDataConfigListByIds(final List<String> idList) {
360         if (idList == null) {
361             return getAllDataConfigList();
362         }
363         return getAllDataConfigList(true, true, false, idList);
364     }
365 
366     /**
367      * Retrieves data crawling configurations with various filtering options.
368      *
369      * @param withLabelType whether to include label type information (currently not used in implementation)
370      * @param withRoleType whether to include role type information (currently not used in implementation)
371      * @param available whether to filter only available configurations
372      * @param idList the list of configuration IDs to retrieve, or null for no ID filtering
373      * @return a list of DataConfig objects matching the criteria
374      */
375     public List<DataConfig> getAllDataConfigList(final boolean withLabelType, final boolean withRoleType, final boolean available,
376             final List<String> idList) {
377         return ComponentUtil.getComponent(DataConfigBhv.class).selectList(cb -> {
378             if (available) {
379                 cb.query().setAvailable_Equal(Constants.T);
380             }
381             if (idList != null) {
382                 cb.query().setId_InScope(idList);
383             }
384             cb.query().setName_NotEqual(ComponentUtil.getFessConfig().getFormAdminDefaultTemplateName());
385             cb.query().addOrderBy_SortOrder_Asc();
386             cb.query().addOrderBy_Name_Asc();
387             cb.fetchFirst(ComponentUtil.getFessConfig().getPageDataConfigMaxFetchSizeAsInteger());
388         });
389     }
390 
391     /**
392      * Retrieves a list of URLs that should be excluded from crawling based on failure counts.
393      * URLs are excluded if they have failed more than the configured failure count threshold.
394      * URLs can also be filtered by failure type using a regular expression pattern.
395      *
396      * @param configId the configuration ID to get excluded URLs for
397      * @return a list of URLs that should be excluded from crawling, or an empty list if none
398      */
399     public List<String> getExcludedUrlList(final String configId) {
400         final FessConfig fessConfig = ComponentUtil.getFessConfig();
401         final int failureCount = fessConfig.getFailureCountThreshold();
402         final String ignoreFailureType = fessConfig.getIgnoreFailureType();
403 
404         if (failureCount < 0) {
405             return Collections.emptyList();
406         }
407 
408         final int count = failureCount;
409         final ListResultBean<FailureUrl> list = ComponentUtil.getComponent(FailureUrlBhv.class).selectList(cb -> {
410             cb.query().setConfigId_Equal(configId);
411             cb.query().setErrorCount_GreaterEqual(count);
412             cb.fetchFirst(fessConfig.getPageFailureUrlMaxFetchSizeAsInteger());
413         });
414         if (list.isEmpty()) {
415             return Collections.emptyList();
416         }
417 
418         Pattern pattern = null;
419         if (StringUtil.isNotBlank(ignoreFailureType)) {
420             pattern = Pattern.compile(ignoreFailureType);
421         }
422         final List<String> urlList = new ArrayList<>();
423         for (final FailureUrl failureUrl : list) {
424             if (pattern != null) {
425                 if (!pattern.matcher(failureUrl.getErrorName()).matches()) {
426                     urlList.add(failureUrl.getUrl());
427                 }
428             } else {
429                 urlList.add(failureUrl.getUrl());
430             }
431         }
432         return urlList;
433     }
434 
435     /**
436      * Retrieves the default crawling configuration template for a given configuration type.
437      * The default template is identified by the configured form admin default template name.
438      *
439      * @param configType the type of configuration (WEB, FILE, or DATA)
440      * @return an OptionalEntity containing the default CrawlingConfig if found, or empty if not found or configType is null
441      */
442     public OptionalEntity<CrawlingConfig> getDefaultConfig(final ConfigType configType) {
443         if (configType == null) {
444             return OptionalEntity.empty();
445         }
446 
447         final String name = ComponentUtil.getFessConfig().getFormAdminDefaultTemplateName();
448 
449         return switch (configType) {
450         case WEB -> {
451             final WebConfigService webConfigService = ComponentUtil.getComponent(WebConfigService.class);
452             yield webConfigService.getWebConfigByName(name).map(o -> (CrawlingConfig) o);
453         }
454         case FILE -> {
455             final FileConfigService fileConfigService = ComponentUtil.getComponent(FileConfigService.class);
456             yield fileConfigService.getFileConfigByName(name).map(o -> (CrawlingConfig) o);
457         }
458         case DATA -> {
459             final DataConfigService dataConfigService = ComponentUtil.getComponent(DataConfigService.class);
460             yield dataConfigService.getDataConfigByName(name).map(o -> (CrawlingConfig) o);
461         }
462         default -> OptionalEntity.empty();
463         };
464     }
465 }