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.web.api.admin.elevateword;
17  
18  import static org.codelibs.core.stream.StreamUtil.stream;
19  import static org.codelibs.fess.app.web.admin.elevateword.AdminElevatewordAction.getElevateWord;
20  
21  import java.io.BufferedReader;
22  import java.io.BufferedWriter;
23  import java.io.InputStream;
24  import java.io.InputStreamReader;
25  import java.io.OutputStreamWriter;
26  import java.io.Reader;
27  import java.io.Writer;
28  import java.nio.file.Files;
29  import java.nio.file.Path;
30  import java.util.List;
31  import java.util.stream.Collectors;
32  
33  import org.apache.logging.log4j.LogManager;
34  import org.apache.logging.log4j.Logger;
35  import org.codelibs.core.concurrent.CommonPoolUtil;
36  import org.codelibs.core.lang.StringUtil;
37  import org.codelibs.fess.Constants;
38  import org.codelibs.fess.app.pager.ElevateWordPager;
39  import org.codelibs.fess.app.service.ElevateWordService;
40  import org.codelibs.fess.app.web.CrudMode;
41  import org.codelibs.fess.app.web.admin.elevateword.UploadForm;
42  import org.codelibs.fess.app.web.api.ApiResult;
43  import org.codelibs.fess.app.web.api.ApiResult.ApiUpdateResponse;
44  import org.codelibs.fess.app.web.api.ApiResult.Status;
45  import org.codelibs.fess.app.web.api.admin.FessApiAdminAction;
46  import org.codelibs.fess.exception.FessSystemException;
47  import org.codelibs.fess.helper.PermissionHelper;
48  import org.codelibs.fess.helper.SuggestHelper;
49  import org.codelibs.fess.opensearch.config.exentity.ElevateWord;
50  import org.codelibs.fess.util.ComponentUtil;
51  import org.lastaflute.web.Execute;
52  import org.lastaflute.web.response.JsonResponse;
53  import org.lastaflute.web.response.StreamResponse;
54  
55  import jakarta.annotation.Resource;
56  
57  /**
58   * API action for admin elevate word management.
59   * Provides RESTful API endpoints for managing elevate word settings in the Fess search engine.
60   * Elevate words boost specific search terms to appear higher in search results.
61   */
62  public class ApiAdminElevatewordAction extends FessApiAdminAction {
63  
64      private static final Logger logger = LogManager.getLogger(ApiAdminElevatewordAction.class);
65  
66      // ===================================================================================
67      //                                                                           Constructor
68      //                                                                           ===========
69  
70      /**
71       * Default constructor.
72       */
73      public ApiAdminElevatewordAction() {
74          super();
75      }
76  
77      // ===================================================================================
78      //                                                                           Attribute
79      //                                                                           =========
80  
81      /** Service for managing elevate word configurations */
82      @Resource
83      private ElevateWordService elevateWordService;
84  
85      /** Helper for managing search suggestions and elevate words */
86      @Resource
87      protected SuggestHelper suggestHelper;
88  
89      // GET /api/admin/elevateword
90      // PUT /api/admin/elevateword
91      /**
92       * Returns list of elevate word settings.
93       * Supports both GET and PUT requests for retrieving paginated elevate word configurations.
94       *
95       * @param body search parameters for filtering and pagination
96       * @return JSON response containing elevate word settings list with pagination info
97       */
98      @Execute
99      public JsonResponse<ApiResult> settings(final SearchBody body) {
100         validateApi(body, messages -> {});
101         final ElevateWordPager pager = copyBeanToNewBean(body, ElevateWordPager.class);
102         final List<ElevateWord> list = elevateWordService.getElevateWordList(pager);
103         return asJson(
104                 new ApiResult.ApiConfigsResponse<EditBody>().settings(list.stream().map(this::createEditBody).collect(Collectors.toList()))
105                         .total(pager.getAllRecordCount())
106                         .status(ApiResult.Status.OK)
107                         .result());
108     }
109 
110     // GET /api/admin/elevateword/{id}
111     /**
112      * Retrieves a specific elevate word setting by ID.
113      *
114      * @param id the ID of the elevate word to retrieve
115      * @return JSON response containing the elevate word configuration
116      */
117     @Execute
118     public JsonResponse<ApiResult> get$setting(final String id) {
119 
120         final ElevateWord entity = elevateWordService.getElevateWord(id).orElseGet(() -> {
121             throwValidationErrorApi(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, id));
122             return null;
123         });
124 
125         final EditBody body = createEditBody(entity);
126         final PermissionHelper permissionHelper = ComponentUtil.getPermissionHelper();
127         body.permissions = stream(entity.getPermissions()).get(stream -> stream.map(s -> permissionHelper.decode(s))
128                 .filter(StringUtil::isNotBlank)
129                 .distinct()
130                 .collect(Collectors.joining("\n")));
131         return asJson(new ApiResult.ApiConfigResponse().setting(body).status(ApiResult.Status.OK).result());
132     }
133 
134     // POST /api/admin/elevateword/setting
135     /**
136      * Creates a new elevate word setting.
137      * Also adds the elevate word to the suggest helper for search enhancement.
138      *
139      * @param body elevate word setting data to create
140      * @return JSON response with created setting ID and status
141      */
142     @Execute
143     public JsonResponse<ApiResult> post$setting(final CreateBody body) {
144         validateApi(body, messages -> {});
145         body.crudMode = CrudMode.CREATE;
146         final ElevateWord entity = getElevateWord(body).orElseGet(() -> {
147             throwValidationErrorApi(messages -> {
148                 messages.addErrorsCrudFailedToCreateInstance(GLOBAL);
149             });
150             return null;
151         });
152         try {
153             elevateWordService.store(entity);
154             suggestHelper.addElevateWord(entity.getSuggestWord(), entity.getReading(), entity.getLabelTypeValues(), entity.getPermissions(),
155                     entity.getBoost(), false);
156         } catch (final Exception e) {
157             logger.warn("Failed to process a request.", e);
158             throwValidationErrorApi(messages -> messages.addErrorsCrudFailedToCreateCrudTable(GLOBAL, buildThrowableMessage(e)));
159         }
160         return asJson(new ApiResult.ApiUpdateResponse().id(entity.getId()).created(true).status(ApiResult.Status.OK).result());
161     }
162 
163     // PUT /api/admin/elevateword/setting
164     /**
165      * Updates an existing elevate word setting.
166      * Refreshes all elevate words in the suggest helper to maintain consistency.
167      *
168      * @param body elevate word setting data to update
169      * @return JSON response with updated setting ID and status
170      */
171     @Execute
172     public JsonResponse<ApiResult> put$setting(final EditBody body) {
173         validateApi(body, messages -> {});
174         body.crudMode = CrudMode.EDIT;
175         final ElevateWord elevateWord = getElevateWord(body).map(entity -> {
176             try {
177                 elevateWordService.store(entity);
178                 suggestHelper.deleteAllElevateWord(false);
179                 suggestHelper.storeAllElevateWords(false);
180             } catch (final Exception e) {
181                 logger.warn("Failed to process a request.", e);
182                 throwValidationErrorApi(messages -> messages.addErrorsCrudFailedToUpdateCrudTable(GLOBAL, buildThrowableMessage(e)));
183             }
184             return entity;
185         }).orElseGet(() -> {
186             throwValidationErrorApi(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, body.id));
187             return null;
188         });
189 
190         return asJson(new ApiUpdateResponse().id(elevateWord.getId()).created(false).status(Status.OK).result());
191     }
192 
193     // DELETE /api/admin/elevateword/setting/{id}
194     /**
195      * Deletes a specific elevate word setting.
196      * Also removes the elevate word from the suggest helper.
197      *
198      * @param id the elevate word setting ID to delete
199      * @return JSON response with deletion status
200      */
201     @Execute
202     public JsonResponse<ApiResult> delete$setting(final String id) {
203         try {
204             elevateWordService.getElevateWord(id).ifPresent(entity -> {
205                 try {
206                     elevateWordService.delete(entity);
207                     suggestHelper.deleteElevateWord(entity.getSuggestWord(), false);
208                     saveInfo(messages -> messages.addSuccessCrudDeleteCrudTable(GLOBAL));
209                 } catch (final Exception e) {
210                     logger.warn("Failed to process a request.", e);
211                     throwValidationErrorApi(messages -> messages.addErrorsCrudFailedToDeleteCrudTable(GLOBAL, buildThrowableMessage(e)));
212                 }
213             }).orElse(() -> {
214                 throwValidationErrorApi(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, id));
215             });
216         } catch (final Exception e) {
217             logger.warn("Failed to process a request.", e);
218             throwValidationErrorApi(messages -> messages.addErrorsCrudFailedToDeleteCrudTable(GLOBAL, buildThrowableMessage(e)));
219         }
220         return asJson(new ApiResult.ApiUpdateResponse().id(id).created(false).status(ApiResult.Status.OK).result());
221     }
222 
223     // PUT /api/admin/elevateword/upload
224     /**
225      * Uploads and imports elevate words from a CSV file.
226      * Processes the file asynchronously and updates the suggest helper.
227      *
228      * @param body upload form containing the CSV file
229      * @return JSON response with upload status
230      */
231     @Execute
232     public JsonResponse<ApiResult> put$upload(final UploadForm body) {
233         validateApi(body, messages -> {});
234         CommonPoolUtil.execute(() -> {
235             try (Reader reader = new BufferedReader(new InputStreamReader(body.elevateWordFile.getInputStream(), getCsvEncoding()))) {
236                 elevateWordService.importCsv(reader);
237                 suggestHelper.storeAllElevateWords(false);
238             } catch (final Exception e) {
239                 throw new FessSystemException("Failed to import data.", e);
240             }
241         });
242         return asJson(new ApiResult.ApiResponse().status(ApiResult.Status.OK).result());
243     }
244 
245     // GET /api/admin/elevateword/download
246     /**
247      * Downloads all elevate words as a CSV file.
248      * Creates a temporary file with the exported data for download.
249      *
250      * @param body download parameters
251      * @return stream response containing the CSV file
252      */
253     @Execute
254     public StreamResponse get$download(final DownloadBody body) {
255         validateApi(body, messages -> {});
256         return asStream("elevate.csv").contentTypeOctetStream().stream(out -> {
257             final Path tempFile = ComponentUtil.getSystemHelper().createTempFile("fess-elevate-", ".csv").toPath();
258             try {
259                 try (Writer writer = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(tempFile), getCsvEncoding()))) {
260                     elevateWordService.exportCsv(writer);
261                 } catch (final Exception e) {
262                     logger.warn("Failed to process a request.", e);
263                     throwValidationErrorApi(messages -> messages.addErrorsFailedToDownloadElevateFile(GLOBAL));
264                 }
265                 try (InputStream in = Files.newInputStream(tempFile)) {
266                     out.write(in);
267                 }
268             } finally {
269                 Files.delete(tempFile);
270             }
271         });
272     }
273 
274     /**
275      * Creates an edit body from an elevate word entity for API responses.
276      * Processes permissions and converts them to a readable format.
277      *
278      * @param entity the elevate word entity to convert
279      * @return edit body containing the entity data
280      */
281     protected EditBody createEditBody(final ElevateWord entity) {
282         final EditBody body = new EditBody();
283         copyBeanToBean(entity, body, copyOp -> {
284             copyOp.excludeNull();
285             copyOp.exclude(Constants.PERMISSIONS);
286         });
287         final PermissionHelper permissionHelper = ComponentUtil.getPermissionHelper();
288         body.permissions = stream(entity.getPermissions()).get(stream -> stream.map(s -> permissionHelper.decode(s))
289                 .filter(StringUtil::isNotBlank)
290                 .distinct()
291                 .collect(Collectors.joining("\n")));
292         return body;
293     }
294 
295     /**
296      * Gets the CSV file encoding from configuration.
297      *
298      * @return the CSV file encoding string
299      */
300     private String getCsvEncoding() {
301         return fessConfig.getCsvFileEncoding();
302     }
303 
304 }