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.app.service;
17  
18  import java.io.IOException;
19  import java.io.Reader;
20  import java.io.Writer;
21  import java.text.DateFormat;
22  import java.text.SimpleDateFormat;
23  import java.time.LocalDateTime;
24  import java.time.format.DateTimeFormatter;
25  import java.util.ArrayList;
26  import java.util.Collections;
27  import java.util.List;
28  import java.util.Set;
29  import java.util.stream.Collectors;
30  
31  import org.apache.logging.log4j.LogManager;
32  import org.apache.logging.log4j.Logger;
33  import org.codelibs.core.CoreLibConstants;
34  import org.codelibs.core.beans.util.BeanUtil;
35  import org.codelibs.core.lang.StringUtil;
36  import org.codelibs.fess.Constants;
37  import org.codelibs.fess.app.pager.CrawlingInfoPager;
38  import org.codelibs.fess.exception.FessSystemException;
39  import org.codelibs.fess.mylasta.direction.FessConfig;
40  import org.codelibs.fess.opensearch.config.cbean.CrawlingInfoCB;
41  import org.codelibs.fess.opensearch.config.exbhv.CrawlingInfoBhv;
42  import org.codelibs.fess.opensearch.config.exbhv.CrawlingInfoParamBhv;
43  import org.codelibs.fess.opensearch.config.exentity.CrawlingInfo;
44  import org.codelibs.fess.opensearch.config.exentity.CrawlingInfoParam;
45  import org.codelibs.fess.util.ComponentUtil;
46  import org.dbflute.bhv.readable.EntityRowHandler;
47  import org.dbflute.cbean.result.ListResultBean;
48  import org.dbflute.cbean.result.PagingResultBean;
49  import org.dbflute.optional.OptionalEntity;
50  
51  import com.orangesignal.csv.CsvConfig;
52  import com.orangesignal.csv.CsvReader;
53  import com.orangesignal.csv.CsvWriter;
54  
55  import jakarta.annotation.Resource;
56  
57  /**
58   * Service class that manages crawling information and parameters.
59   * This service provides CRUD operations for crawling sessions and their associated parameters,
60   * including session management, cleanup operations, and CSV import/export functionality.
61   */
62  public class CrawlingInfoService {
63  
64      private static final Logger logger = LogManager.getLogger(CrawlingInfoService.class);
65  
66      /**
67       * Creates a new instance of CrawlingInfoService.
68       */
69      public CrawlingInfoService() {
70      }
71  
72      /**
73       * Behavior handler for CrawlingInfoParam entities.
74       * Used to perform database operations on crawling session parameters.
75       */
76      @Resource
77      protected CrawlingInfoParamBhv crawlingInfoParamBhv;
78  
79      /**
80       * Behavior handler for CrawlingInfo entities.
81       * Used to perform database operations on crawling session information.
82       */
83      @Resource
84      protected CrawlingInfoBhv crawlingInfoBhv;
85  
86      /**
87       * Fess configuration object containing application settings.
88       * Used to access configuration values for pagination, limits, and other settings.
89       */
90      @Resource
91      protected FessConfig fessConfig;
92  
93      /**
94       * Retrieves a paginated list of crawling information records based on the provided pager criteria.
95       * The results are ordered by creation time in descending order and the pager is updated with
96       * pagination metadata including total count and page number list.
97       *
98       * @param crawlingInfoPager the pager object containing search criteria and pagination settings
99       * @return a list of CrawlingInfo entities matching the criteria
100      */
101     public List<CrawlingInfo> getCrawlingInfoList(final CrawlingInfoPager crawlingInfoPager) {
102 
103         final PagingResultBean<CrawlingInfo> crawlingInfoList = crawlingInfoBhv.selectPage(cb -> {
104             cb.paging(crawlingInfoPager.getPageSize(), crawlingInfoPager.getCurrentPageNumber());
105             setupListCondition(cb, crawlingInfoPager);
106         });
107 
108         // update pager
109         BeanUtil.copyBeanToBean(crawlingInfoList, crawlingInfoPager, option -> option.include(Constants.PAGER_CONVERSION_RULE));
110         crawlingInfoPager.setPageNumberList(
111                 crawlingInfoList.pageRange(op -> op.rangeSize(fessConfig.getPagingPageRangeSizeAsInteger())).createPageNumberList());
112 
113         return crawlingInfoList;
114     }
115 
116     /**
117      * Retrieves a single crawling information record by its unique identifier.
118      *
119      * @param id the unique identifier of the crawling information record
120      * @return an OptionalEntity containing the CrawlingInfo if found, empty otherwise
121      */
122     public OptionalEntity<CrawlingInfo> getCrawlingInfo(final String id) {
123         return crawlingInfoBhv.selectByPK(id);
124     }
125 
126     /**
127      * Stores (inserts or updates) a crawling information record.
128      * Sets up the store conditions including creation time if not already set,
129      * then performs an insert or update operation with immediate refresh.
130      *
131      * @param crawlingInfo the crawling information entity to store
132      * @throws FessSystemException if the crawling information is null
133      */
134     public void store(final CrawlingInfo crawlingInfo) {
135         setupStoreCondition(crawlingInfo);
136 
137         crawlingInfoBhv.insertOrUpdate(crawlingInfo, op -> op.setRefreshPolicy(Constants.TRUE));
138 
139     }
140 
141     /**
142      * Deletes a crawling information record and all its associated parameters.
143      * First deletes all related CrawlingInfoParam records, then deletes the main record
144      * with immediate refresh to ensure consistency.
145      *
146      * @param crawlingInfo the crawling information entity to delete
147      */
148     public void delete(final CrawlingInfo crawlingInfo) {
149         setupDeleteCondition(crawlingInfo);
150 
151         crawlingInfoBhv.delete(crawlingInfo, op -> op.setRefreshPolicy(Constants.TRUE));
152 
153     }
154 
155     /**
156      * Sets up the database query conditions for listing crawling information records.
157      * Applies filters based on the pager criteria such as ID and session ID,
158      * and orders results by creation time in descending order.
159      *
160      * @param cb the condition bean for building the database query
161      * @param crawlingInfoPager the pager containing filter criteria
162      */
163     protected void setupListCondition(final CrawlingInfoCB cb, final CrawlingInfoPager crawlingInfoPager) {
164         if (crawlingInfoPager.id != null) {
165             cb.query().docMeta().setId_Equal(crawlingInfoPager.id);
166         }
167         // TODO Long, Integer, String supported only.
168         if (StringUtil.isNotBlank(crawlingInfoPager.sessionId)) {
169             cb.query().setSessionId_Match(crawlingInfoPager.sessionId);
170         }
171         cb.query().addOrderBy_CreatedTime_Desc();
172     }
173 
174     /**
175      * Sets up the conditions for storing a crawling information record.
176      * Validates that the entity is not null and sets the creation time if not already present.
177      *
178      * @param crawlingInfo the crawling information entity to prepare for storage
179      * @throws FessSystemException if the crawling information is null
180      */
181     protected void setupStoreCondition(final CrawlingInfo crawlingInfo) {
182         if (crawlingInfo == null) {
183             throw new FessSystemException("Crawling Session is null.");
184         }
185         final long now = ComponentUtil.getSystemHelper().getCurrentTimeAsLong();
186         if (crawlingInfo.getCreatedTime() == null) {
187             crawlingInfo.setCreatedTime(now);
188         }
189     }
190 
191     /**
192      * Sets up the conditions for deleting a crawling information record.
193      * Ensures all associated CrawlingInfoParam records are deleted first to maintain referential integrity.
194      *
195      * @param crawlingInfo the crawling information entity to prepare for deletion
196      */
197     protected void setupDeleteCondition(final CrawlingInfo crawlingInfo) {
198         crawlingInfoParamBhv.queryDelete(cb -> cb.query().setCrawlingInfoId_Equal(crawlingInfo.getId()));
199     }
200 
201     /**
202      * Deletes crawling sessions that expired before the specified date.
203      * Excludes the active session and optionally filters by name.
204      * This method performs batch deletion of both parameters and session records.
205      *
206      * @param activeSessionId the session ID to exclude from deletion (can be null)
207      * @param name optional name filter for sessions to delete (can be null or blank)
208      * @param date the expiration time threshold - sessions expired before this time will be deleted
209      */
210     public void deleteSessionIdsBefore(final String activeSessionId, final String name, final long date) {
211         final List<CrawlingInfo> crawlingInfoList = crawlingInfoBhv.selectList(cb -> {
212             cb.query().filtered((cq, cf) -> {
213                 cq.setExpiredTime_LessEqual(date);
214                 if (StringUtil.isNotBlank(name)) {
215                     cf.setName_Equal(name);
216                 }
217                 if (activeSessionId != null) {
218                     cf.setSessionId_NotEqual(activeSessionId);
219                 }
220 
221             });
222 
223             cb.fetchFirst(fessConfig.getPageCrawlingInfoMaxFetchSizeAsInteger());
224             cb.specify().columnId();
225         });
226         if (!crawlingInfoList.isEmpty()) {
227             final List<String> crawlingInfoIdList = new ArrayList<>();
228             for (final CrawlingInfo cs : crawlingInfoList) {
229                 crawlingInfoIdList.add(cs.getId());
230             }
231             crawlingInfoParamBhv.queryDelete(cb2 -> cb2.query().setCrawlingInfoId_InScope(crawlingInfoIdList));
232             crawlingInfoBhv.batchDelete(crawlingInfoList, op -> op.setRefreshPolicy(Constants.TRUE));
233         }
234     }
235 
236     /**
237      * Stores a list of crawling information parameters in batch.
238      * Sets the creation time for any parameters that don't have it set,
239      * then performs a batch insert operation with immediate refresh.
240      *
241      * @param crawlingInfoParamList the list of crawling information parameters to store
242      * @throws FessSystemException if the parameter list is null
243      */
244     public void storeInfo(final List<CrawlingInfoParam> crawlingInfoParamList) {
245         if (crawlingInfoParamList == null) {
246             throw new FessSystemException("Crawling Session Info is null.");
247         }
248 
249         final long now = ComponentUtil.getSystemHelper().getCurrentTimeAsLong();
250         for (final CrawlingInfoParam crawlingInfoParam : crawlingInfoParamList) {
251             if (crawlingInfoParam.getCreatedTime() == null) {
252                 crawlingInfoParam.setCreatedTime(now);
253             }
254         }
255         crawlingInfoParamBhv.batchInsert(crawlingInfoParamList, op -> op.setRefreshPolicy(Constants.TRUE));
256     }
257 
258     /**
259      * Retrieves all parameters associated with a specific crawling information record.
260      * Results are ordered by creation time in ascending order and limited by configuration.
261      *
262      * @param id the unique identifier of the crawling information record
263      * @return a list of CrawlingInfoParam entities associated with the specified crawling info
264      */
265     public List<CrawlingInfoParam> getCrawlingInfoParamList(final String id) {
266         return crawlingInfoParamBhv.selectList(cb -> {
267             cb.query().setCrawlingInfoId_Equal(id);
268             cb.query().addOrderBy_CreatedTime_Asc();
269             cb.fetchFirst(fessConfig.getPageCrawlingInfoParamMaxFetchSizeAsInteger());
270         });
271     }
272 
273     /**
274      * Retrieves the parameters from the most recent crawling session for a given session ID.
275      * Returns an empty list if no crawling information is found for the session.
276      *
277      * @param sessionId the session identifier to find the latest crawling parameters for
278      * @return a list of CrawlingInfoParam entities from the latest session, or empty list if none found
279      */
280     public List<CrawlingInfoParam> getLastCrawlingInfoParamList(final String sessionId) {
281         final CrawlingInfo crawlingInfo = getLast(sessionId);
282         if (crawlingInfo == null) {
283             return Collections.emptyList();
284         }
285         final FessConfig fessConfig = ComponentUtil.getFessConfig();
286         return crawlingInfoParamBhv.selectList(cb -> {
287             cb.query().setCrawlingInfoId_Equal(crawlingInfo.getId());
288             cb.query().addOrderBy_CreatedTime_Asc();
289             cb.paging(fessConfig.getPageCrawlingInfoParamMaxFetchSizeAsInteger(), 1);
290         });
291     }
292 
293     /**
294      * Deletes all crawling sessions and their parameters except for the specified active sessions.
295      * This is a cleanup operation that removes inactive session data while preserving active ones.
296      *
297      * @param activeSessionId a set of session IDs to preserve during the cleanup operation
298      */
299     public void deleteOldSessions(final Set<String> activeSessionId) {
300         final List<CrawlingInfo> activeSessionList =
301                 activeSessionId.isEmpty() ? Collections.emptyList() : crawlingInfoBhv.selectList(cb -> {
302                     cb.query().setSessionId_InScope(activeSessionId);
303                     cb.fetchFirst(fessConfig.getPageCrawlingInfoMaxFetchSizeAsInteger());
304                     cb.specify().columnId();
305                 });
306         final List<String> idList = activeSessionList.stream().map(CrawlingInfo::getId).collect(Collectors.toList());
307         crawlingInfoParamBhv.queryDelete(cb1 -> cb1.query().filtered((cq, cf) -> {
308             cq.matchAll();
309             if (!idList.isEmpty()) {
310                 cf.not(subCf -> subCf.setCrawlingInfoId_InScope(idList));
311             }
312         }));
313         crawlingInfoBhv.queryDelete(cb2 -> cb2.query().filtered((cq, cf) -> {
314             cq.matchAll();
315             if (!idList.isEmpty()) {
316                 cf.not(subCf -> subCf.setId_InScope(idList));
317             }
318         }));
319     }
320 
321     /**
322      * Imports crawling information and parameters from a CSV file.
323      * The CSV format expected is: SessionId, SessionCreatedTime, Key, Value, CreatedTime.
324      * Creates new crawling sessions if they don't exist and adds parameters to them.
325      *
326      * @param reader the Reader containing CSV data to import
327      */
328     public void importCsv(final Reader reader) {
329         @SuppressWarnings("resource")
330         final CsvReader csvReader = new CsvReader(reader, new CsvConfig());
331         final DateFormat formatter = new SimpleDateFormat(CoreLibConstants.DATE_FORMAT_ISO_8601_EXTEND);
332         try {
333             List<String> list;
334             csvReader.readValues(); // ignore header
335             while ((list = csvReader.readValues()) != null) {
336                 try {
337                     final String sessionId = list.get(0);
338                     CrawlingInfo crawlingInfo = crawlingInfoBhv.selectEntity(cb -> {
339                         cb.query().setSessionId_Equal(sessionId);
340                         cb.specify().columnSessionId();
341                     }).orElse(null);//TODO
342                     if (crawlingInfo == null) {
343                         crawlingInfo = new CrawlingInfo();
344                         crawlingInfo.setSessionId(list.get(0));
345                         crawlingInfo.setCreatedTime(formatter.parse(list.get(1)).getTime());
346                         crawlingInfoBhv.insert(crawlingInfo, op -> op.setRefreshPolicy(Constants.TRUE));
347                     }
348 
349                     final CrawlingInfoParam entity = new CrawlingInfoParam();
350                     entity.setCrawlingInfoId(crawlingInfo.getId());
351                     entity.setKey(list.get(2));
352                     entity.setValue(list.get(3));
353                     entity.setCreatedTime(formatter.parse(list.get(4)).getTime());
354                     crawlingInfoParamBhv.insert(entity, op -> op.setRefreshPolicy(Constants.TRUE));
355                 } catch (final Exception e) {
356                     logger.warn("Failed to read a click log: {}", list, e);
357                 }
358             }
359         } catch (final IOException e) {
360             logger.warn("Failed to read a click log.", e);
361         }
362     }
363 
364     /**
365      * Exports all crawling information parameters to CSV format.
366      * The CSV output includes: SessionId, SessionCreatedTime, Key, Value, CreatedTime.
367      * Uses cursor-based selection to handle large datasets efficiently.
368      *
369      * @param writer the Writer to output CSV data to
370      */
371     public void exportCsv(final Writer writer) {
372         final CsvConfig cfg = new CsvConfig(',', '"', '"');
373         cfg.setEscapeDisabled(false);
374         cfg.setQuoteDisabled(false);
375         @SuppressWarnings("resource")
376         final CsvWriter csvWriter = new CsvWriter(writer, cfg);
377         try {
378             final List<String> list = new ArrayList<>();
379             list.add("SessionId");
380             list.add("SessionCreatedTime");
381             list.add("Key");
382             list.add("Value");
383             list.add("CreatedTime");
384             csvWriter.writeValues(list);
385             final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(CoreLibConstants.DATE_FORMAT_ISO_8601_EXTEND);
386             crawlingInfoParamBhv.selectCursor(cb -> cb.query().matchAll(), new EntityRowHandler<CrawlingInfoParam>() {
387                 @Override
388                 public void handle(final CrawlingInfoParam entity) {
389                     final List<String> list = new ArrayList<>();
390                     entity.getCrawlingInfo().ifPresent(crawlingInfo -> {
391                         addToList(list, crawlingInfo.getSessionId());
392                         addToList(list, crawlingInfo.getCreatedTime());
393                     });
394                     // TODO
395                     if (!entity.getCrawlingInfo().isPresent()) {
396                         addToList(list, "");
397                         addToList(list, "");
398                     }
399                     addToList(list, entity.getKey());
400                     addToList(list, entity.getValue());
401                     addToList(list, entity.getCreatedTime());
402                     try {
403                         csvWriter.writeValues(list);
404                     } catch (final IOException e) {
405                         logger.warn("Failed to write a crawling session info: {}", entity, e);
406                     }
407                 }
408 
409                 private void addToList(final List<String> list, final Object value) {
410                     if (value == null) {
411                         list.add(StringUtil.EMPTY);
412                     } else if (value instanceof LocalDateTime) {
413                         list.add(((LocalDateTime) value).format(formatter));
414                     } else {
415                         list.add(value.toString());
416                     }
417                 }
418             });
419             csvWriter.flush();
420         } catch (final IOException e) {
421             logger.warn("Failed to write a crawling session info.", e);
422         }
423     }
424 
425     /**
426      * Deletes all crawling information records and their parameters that expired before the specified date.
427      * This is a bulk cleanup operation for removing old session data.
428      *
429      * @param date the expiration time threshold - records expired before this time will be deleted
430      */
431     public void deleteBefore(final long date) {
432         crawlingInfoBhv.selectBulk(cb -> cb.query().setExpiredTime_LessThan(date), list -> {
433             final List<String> idList = list.stream().map(CrawlingInfo::getId).collect(Collectors.toList());
434             crawlingInfoParamBhv.queryDelete(cb1 -> cb1.query().setCrawlingInfoId_InScope(idList));
435             crawlingInfoBhv.queryDelete(cb2 -> cb2.query().setExpiredTime_LessThan(date));
436         });
437     }
438 
439     /**
440      * Retrieves the most recent crawling information record for a given session ID.
441      * Orders by creation time in descending order and returns only the first result.
442      *
443      * @param sessionId the session identifier to find the latest crawling information for
444      * @return the most recent CrawlingInfo entity for the session, or null if none found
445      */
446     public CrawlingInfo getLast(final String sessionId) {
447         final ListResultBean<CrawlingInfo> list = crawlingInfoBhv.selectList(cb -> {
448             cb.query().setSessionId_Equal(sessionId);
449             cb.query().addOrderBy_CreatedTime_Desc();
450             cb.fetchFirst(1);
451         });
452         if (list.isEmpty()) {
453             return null;
454         }
455         return list.get(0);
456     }
457 
458 }