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 static org.codelibs.core.stream.StreamUtil.split;
19  import static org.codelibs.core.stream.StreamUtil.stream;
20  
21  import java.io.IOException;
22  import java.io.Reader;
23  import java.io.Writer;
24  import java.util.ArrayList;
25  import java.util.List;
26  import java.util.stream.Collectors;
27  
28  import org.apache.logging.log4j.LogManager;
29  import org.apache.logging.log4j.Logger;
30  import org.codelibs.core.beans.util.BeanUtil;
31  import org.codelibs.core.lang.StringUtil;
32  import org.codelibs.fess.Constants;
33  import org.codelibs.fess.app.pager.ElevateWordPager;
34  import org.codelibs.fess.helper.PermissionHelper;
35  import org.codelibs.fess.mylasta.direction.FessConfig;
36  import org.codelibs.fess.opensearch.client.SearchEngineClient;
37  import org.codelibs.fess.opensearch.config.cbean.ElevateWordCB;
38  import org.codelibs.fess.opensearch.config.exbhv.ElevateWordBhv;
39  import org.codelibs.fess.opensearch.config.exbhv.ElevateWordToLabelBhv;
40  import org.codelibs.fess.opensearch.config.exbhv.LabelTypeBhv;
41  import org.codelibs.fess.opensearch.config.exentity.ElevateWord;
42  import org.codelibs.fess.opensearch.config.exentity.ElevateWordToLabel;
43  import org.codelibs.fess.opensearch.config.exentity.LabelType;
44  import org.codelibs.fess.util.ComponentUtil;
45  import org.dbflute.bhv.readable.EntityRowHandler;
46  import org.dbflute.cbean.result.PagingResultBean;
47  import org.dbflute.optional.OptionalEntity;
48  
49  import com.orangesignal.csv.CsvConfig;
50  import com.orangesignal.csv.CsvReader;
51  import com.orangesignal.csv.CsvWriter;
52  
53  import jakarta.annotation.Resource;
54  
55  /**
56   * Service class for managing elevate words functionality.
57   * Elevate words are used to boost the relevance of documents containing specific terms in search results.
58   * This service provides CRUD operations, CSV import/export, and related functionality.
59   */
60  public class ElevateWordService {
61  
62      /** Logger instance for this class */
63      private static final Logger logger = LogManager.getLogger(ElevateWordService.class);
64  
65      /**
66       * Default constructor.
67       */
68      public ElevateWordService() {
69          // Default constructor
70      }
71  
72      /** Behavior handler for ElevateWordToLabel entity operations */
73      @Resource
74      protected ElevateWordToLabelBhv elevateWordToLabelBhv;
75  
76      /** Behavior handler for ElevateWord entity operations */
77      @Resource
78      protected ElevateWordBhv elevateWordBhv;
79  
80      /** Behavior handler for LabelType entity operations */
81      @Resource
82      protected LabelTypeBhv labelTypeBhv;
83  
84      /** Configuration settings for Fess application */
85      @Resource
86      protected FessConfig fessConfig;
87  
88      /** Client for interacting with the search engine */
89      @Resource
90      protected SearchEngineClient searchEngineClient;
91  
92      /**
93       * Retrieves a paginated list of elevate words based on the provided pager criteria.
94       *
95       * @param elevateWordPager the pager containing pagination and filtering criteria
96       * @return list of elevate words matching the criteria
97       */
98      public List<ElevateWord> getElevateWordList(final ElevateWordPager elevateWordPager) {
99  
100         final PagingResultBean<ElevateWord> elevateWordList = elevateWordBhv.selectPage(cb -> {
101             cb.paging(elevateWordPager.getPageSize(), elevateWordPager.getCurrentPageNumber());
102             setupListCondition(cb, elevateWordPager);
103         });
104 
105         // update pager
106         BeanUtil.copyBeanToBean(elevateWordList, elevateWordPager, option -> option.include(Constants.PAGER_CONVERSION_RULE));
107         elevateWordPager.setPageNumberList(elevateWordList.pageRange(op -> {
108             op.rangeSize(fessConfig.getPagingPageRangeSizeAsInteger());
109         }).createPageNumberList());
110 
111         return elevateWordList;
112     }
113 
114     /**
115      * Retrieves a specific elevate word by its ID, including associated label type information.
116      *
117      * @param id the unique identifier of the elevate word
118      * @return OptionalEntity containing the elevate word if found, or empty if not found
119      */
120     public OptionalEntity<ElevateWord> getElevateWord(final String id) {
121         return elevateWordBhv.selectByPK(id).map(entity -> {
122 
123             final List<ElevateWordToLabel> wctltmList = elevateWordToLabelBhv.selectList(wctltmCb -> {
124                 wctltmCb.query().setElevateWordId_Equal(entity.getId());
125                 wctltmCb.fetchFirst(fessConfig.getPageLabeltypeMaxFetchSizeAsInteger());
126             });
127             if (!wctltmList.isEmpty()) {
128                 final List<String> labelTypeIds = new ArrayList<>(wctltmList.size());
129                 for (final ElevateWordToLabel mapping : wctltmList) {
130                     labelTypeIds.add(mapping.getLabelTypeId());
131                 }
132                 entity.setLabelTypeIds(labelTypeIds.toArray(new String[labelTypeIds.size()]));
133             }
134             return entity;
135         });
136     }
137 
138     /**
139      * Stores (inserts or updates) an elevate word and manages its associated label type mappings.
140      *
141      * @param elevateWord the elevate word entity to store
142      */
143     public void store(final ElevateWord elevateWord) {
144         final boolean isNew = elevateWord.getId() == null;
145         final String[] labelTypeIds = elevateWord.getLabelTypeIds();
146 
147         elevateWordBhv.insertOrUpdate(elevateWord, op -> {
148             op.setRefreshPolicy(Constants.TRUE);
149         });
150         final String elevateWordId = elevateWord.getId();
151         if (labelTypeIds != null) {
152             if (isNew) {
153                 final List<ElevateWordToLabel> wctltmList = new ArrayList<>();
154                 for (final String id : labelTypeIds) {
155                     final ElevateWordToLabel mapping = new ElevateWordToLabel();
156                     mapping.setElevateWordId(elevateWordId);
157                     mapping.setLabelTypeId(id);
158                     wctltmList.add(mapping);
159                 }
160                 elevateWordToLabelBhv.batchInsert(wctltmList, op -> {
161                     op.setRefreshPolicy(Constants.TRUE);
162                 });
163             } else {
164                 final List<ElevateWordToLabel> list = elevateWordToLabelBhv.selectList(wctltmCb -> {
165                     wctltmCb.query().setElevateWordId_Equal(elevateWordId);
166                     wctltmCb.fetchFirst(fessConfig.getPageLabeltypeMaxFetchSizeAsInteger());
167                 });
168                 final List<ElevateWordToLabel> newList = new ArrayList<>();
169                 final List<ElevateWordToLabel> matchedList = new ArrayList<>();
170                 for (final String id : labelTypeIds) {
171                     boolean exist = false;
172                     for (final ElevateWordToLabel mapping : list) {
173                         if (mapping.getLabelTypeId().equals(id)) {
174                             exist = true;
175                             matchedList.add(mapping);
176                             break;
177                         }
178                     }
179                     if (!exist) {
180                         // new
181                         final ElevateWordToLabel mapping = new ElevateWordToLabel();
182                         mapping.setElevateWordId(elevateWordId);
183                         mapping.setLabelTypeId(id);
184                         newList.add(mapping);
185                     }
186                 }
187                 list.removeAll(matchedList);
188                 elevateWordToLabelBhv.batchInsert(newList, op -> {
189                     op.setRefreshPolicy(Constants.TRUE);
190                 });
191                 elevateWordToLabelBhv.batchDelete(list, op -> {
192                     op.setRefreshPolicy(Constants.TRUE);
193                 });
194             }
195         }
196     }
197 
198     /**
199      * Deletes an elevate word from the system.
200      *
201      * @param elevateWord the elevate word entity to delete
202      */
203     public void delete(final ElevateWord elevateWord) {
204 
205         elevateWordBhv.delete(elevateWord, op -> {
206             op.setRefreshPolicy(Constants.TRUE);
207         });
208 
209     }
210 
211     /**
212      * Sets up the condition builder for querying elevate words based on pager criteria.
213      *
214      * @param cb the condition builder to configure
215      * @param elevateWordPager the pager containing search criteria
216      */
217     protected void setupListCondition(final ElevateWordCB cb, final ElevateWordPager elevateWordPager) {
218         if (elevateWordPager.id != null) {
219             cb.query().docMeta().setId_Equal(elevateWordPager.id);
220         }
221         // TODO Long, Integer, String supported only.
222 
223         // setup condition
224         cb.query().addOrderBy_SuggestWord_Asc();
225 
226         // search
227 
228     }
229 
230     /**
231      * Imports elevate words from a CSV file.
232      * Expected CSV format: SuggestWord, Reading, Permissions, Labels, Boost
233      *
234      * @param reader the Reader containing CSV data to import
235      */
236     public void importCsv(final Reader reader) {
237         final PermissionHelper permissionHelper = ComponentUtil.getPermissionHelper();
238         final CsvConfig cfg = new CsvConfig(',', '"', '"');
239         cfg.setEscapeDisabled(false);
240         cfg.setQuoteDisabled(false);
241         @SuppressWarnings("resource")
242         final CsvReader csvReader = new CsvReader(reader, cfg);
243         try {
244             List<String> list;
245             csvReader.readValues(); // ignore header
246             while ((list = csvReader.readValues()) != null) {
247                 final String suggestWord = getValue(list, 0);
248                 if (StringUtil.isBlank(suggestWord)) {
249                     // skip
250                     continue;
251                 }
252                 try {
253                     final String[] permissions = split(getValue(list, 2), ",").get(stream -> stream.map(permissionHelper::encode)
254                             .filter(StringUtil::isNotBlank)
255                             .distinct()
256                             .toArray(n -> new String[n]));
257                     final String[] labels = split(getValue(list, 3), ",")
258                             .get(stream -> stream.filter(StringUtil::isNotBlank).distinct().toArray(n -> new String[n]));
259                     ElevateWord elevateWord = elevateWordBhv.selectEntity(cb -> {
260                         cb.query().setSuggestWord_Equal(suggestWord);
261                         if (permissions.length > 0) {
262                             cb.query().setPermissions_InScope(stream(permissions).get(stream -> stream.collect(Collectors.toList())));
263                         }
264                     }).orElse(null);
265                     final String reading = getValue(list, 1);
266                     final String boost = getValue(list, 4);
267                     final long now = ComponentUtil.getSystemHelper().getCurrentTimeAsLong();
268                     if (elevateWord == null) {
269                         elevateWord = new ElevateWord();
270                         elevateWord.setSuggestWord(suggestWord);
271                         elevateWord.setReading(reading);
272                         elevateWord.setPermissions(permissions);
273                         elevateWord.setBoost(StringUtil.isBlank(boost) ? 1.0f : Float.parseFloat(boost));
274                         elevateWord.setCreatedBy(Constants.SYSTEM_USER);
275                         elevateWord.setCreatedTime(now);
276                         elevateWordBhv.insert(elevateWord);
277                         final String id = elevateWord.getId();
278                         final List<ElevateWordToLabel> mappingList = stream(labels)
279                                 .get(stream -> stream.map(l -> labelTypeBhv.selectEntity(cb -> cb.query().setValue_Equal(l)).map(e -> {
280                                     final ElevateWordToLabel m = new ElevateWordToLabel();
281                                     m.setElevateWordId(id);
282                                     m.setLabelTypeId(e.getId());
283                                     return m;
284                                 }).orElse(null)).filter(e -> e != null).collect(Collectors.toList()));
285                         if (!mappingList.isEmpty()) {
286                             elevateWordToLabelBhv.batchInsert(mappingList);
287                         }
288                     } else if (StringUtil.isBlank(reading) && StringUtil.isBlank(boost)) {
289                         elevateWordBhv.delete(elevateWord);
290                         final String id = elevateWord.getId();
291                         elevateWordToLabelBhv.queryDelete(cb -> cb.query().setElevateWordId_Equal(id));
292                     } else {
293                         elevateWord.setReading(reading);
294                         elevateWord.setPermissions(permissions);
295                         elevateWord.setBoost(StringUtil.isBlank(boost) ? 1.0f : Float.parseFloat(boost));
296                         elevateWord.setUpdatedBy(Constants.SYSTEM_USER);
297                         elevateWord.setUpdatedTime(now);
298                         elevateWordBhv.update(elevateWord);
299                         final String id = elevateWord.getId();
300                         final List<ElevateWordToLabel> mappingList = stream(labels)
301                                 .get(stream -> stream.map(l -> labelTypeBhv.selectEntity(cb -> cb.query().setValue_Equal(l)).map(e -> {
302                                     final List<ElevateWordToLabel> mList = elevateWordToLabelBhv.selectList(cb -> {
303                                         cb.query().setElevateWordId_Equal(id);
304                                         cb.query().setLabelTypeId_Equal(e.getId());
305                                     });
306                                     if (!mList.isEmpty()) {
307                                         return null;
308                                     }
309                                     final ElevateWordToLabel m = new ElevateWordToLabel();
310                                     m.setElevateWordId(id);
311                                     m.setLabelTypeId(e.getId());
312                                     return m;
313                                 }).orElse(null)).filter(e -> e != null).collect(Collectors.toList()));
314                         if (!mappingList.isEmpty()) {
315                             elevateWordToLabelBhv.batchInsert(mappingList);
316                         }
317                     }
318                 } catch (final Exception e) {
319                     logger.warn("Failed to read a suggest elevate word: {}", list, e);
320                 }
321             }
322             elevateWordBhv.refresh();
323         } catch (final IOException e) {
324             logger.warn("Failed to read a suggest elevate word.", e);
325         }
326     }
327 
328     /**
329      * Exports elevate words to a CSV file.
330      * CSV format: SuggestWord, Reading, Permissions, Labels, Boost
331      *
332      * @param writer the Writer to output CSV data to
333      */
334     public void exportCsv(final Writer writer) {
335         final PermissionHelper permissionHelper = ComponentUtil.getPermissionHelper();
336         final CsvConfig cfg = new CsvConfig(',', '"', '"');
337         cfg.setEscapeDisabled(false);
338         cfg.setQuoteDisabled(false);
339         @SuppressWarnings("resource")
340         final CsvWriter csvWriter = new CsvWriter(writer, cfg);
341         try {
342             final List<String> list = new ArrayList<>();
343             list.add("SuggestWord");
344             list.add("Reading");
345             list.add("Permissions");
346             list.add("Labels");
347             list.add("Boost");
348             csvWriter.writeValues(list);
349 
350             elevateWordBhv.selectCursor(cb -> cb.query().matchAll(), new EntityRowHandler<ElevateWord>() {
351                 @Override
352                 public void handle(final ElevateWord entity) {
353                     final List<String> list = new ArrayList<>();
354                     final String permissions = stream(entity.getPermissions()).get(stream -> stream.map(s -> permissionHelper.decode(s))
355                             .filter(StringUtil::isNotBlank)
356                             .distinct()
357                             .collect(Collectors.joining(",")));
358                     final String labels = elevateWordToLabelBhv.selectList(cb -> cb.query().setElevateWordId_Equal(entity.getId()))
359                             .stream()
360                             .map(e -> labelTypeBhv.selectByPK(e.getLabelTypeId())
361                                     .map(LabelType::getValue)
362                                     .filter(StringUtil::isNotBlank)
363                                     .orElse(null))
364                             .distinct()
365                             .sorted()
366                             .collect(Collectors.joining(","));
367                     addToList(list, entity.getSuggestWord());
368                     addToList(list, entity.getReading());
369                     addToList(list, permissions);
370                     addToList(list, labels);
371                     addToList(list, entity.getBoost());
372                     try {
373                         csvWriter.writeValues(list);
374                     } catch (final IOException e) {
375                         logger.warn("Failed to write a suggest elevate word: {}", entity, e);
376                     }
377                 }
378 
379                 private void addToList(final List<String> list, final Object value) {
380                     if (value == null) {
381                         list.add(StringUtil.EMPTY);
382                     } else {
383                         list.add(value.toString());
384                     }
385                 }
386             });
387 
388             csvWriter.flush();
389         } catch (final IOException e) {
390             logger.warn("Failed to write a suggest elevate word.", e);
391         }
392     }
393 
394     /**
395      * Safely retrieves a value from a list at the specified index, handling bounds checking
396      * and cleaning up quoted strings.
397      *
398      * @param list the list to retrieve the value from
399      * @param index the index of the value to retrieve
400      * @return the cleaned value at the specified index, or empty string if index is out of bounds
401      */
402     static String getValue(final List<String> list, final int index) {
403         if (index >= list.size()) {
404             return StringUtil.EMPTY;
405         }
406         String item = list.get(index).trim();
407         if (StringUtil.isBlank(item)) {
408             return StringUtil.EMPTY;
409         }
410         if (item.length() > 1 && item.charAt(0) == '"' && item.charAt(item.length() - 1) == '"') {
411             item = item.substring(1, item.length() - 1);
412         }
413         return item;
414     }
415 
416 }