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.badword;
17  
18  import static org.codelibs.fess.app.web.admin.badword.AdminBadwordAction.getBadWord;
19  
20  import java.io.BufferedReader;
21  import java.io.BufferedWriter;
22  import java.io.InputStream;
23  import java.io.InputStreamReader;
24  import java.io.OutputStreamWriter;
25  import java.io.Reader;
26  import java.io.Writer;
27  import java.nio.file.Files;
28  import java.nio.file.Path;
29  import java.util.List;
30  import java.util.stream.Collectors;
31  
32  import org.apache.logging.log4j.LogManager;
33  import org.apache.logging.log4j.Logger;
34  import org.codelibs.core.concurrent.CommonPoolUtil;
35  import org.codelibs.fess.app.pager.BadWordPager;
36  import org.codelibs.fess.app.service.BadWordService;
37  import org.codelibs.fess.app.web.CrudMode;
38  import org.codelibs.fess.app.web.admin.badword.UploadForm;
39  import org.codelibs.fess.app.web.api.ApiResult;
40  import org.codelibs.fess.app.web.api.ApiResult.ApiUpdateResponse;
41  import org.codelibs.fess.app.web.api.ApiResult.Status;
42  import org.codelibs.fess.app.web.api.admin.FessApiAdminAction;
43  import org.codelibs.fess.exception.FessSystemException;
44  import org.codelibs.fess.helper.SuggestHelper;
45  import org.codelibs.fess.opensearch.config.exentity.BadWord;
46  import org.codelibs.fess.util.ComponentUtil;
47  import org.lastaflute.web.Execute;
48  import org.lastaflute.web.response.JsonResponse;
49  import org.lastaflute.web.response.StreamResponse;
50  
51  import jakarta.annotation.Resource;
52  
53  /**
54   * API action for admin bad word management.
55   * Provides REST endpoints for managing bad words in the Fess search engine.
56   */
57  public class ApiAdminBadwordAction extends FessApiAdminAction {
58  
59      /**
60       * Default constructor.
61       */
62      public ApiAdminBadwordAction() {
63          super();
64      }
65  
66      private static final Logger logger = LogManager.getLogger(ApiAdminBadwordAction.class);
67  
68      @Resource
69      private BadWordService badWordService;
70  
71      /** Helper for managing search suggestions and bad words */
72      @Resource
73      protected SuggestHelper suggestHelper;
74  
75      /**
76       * Retrieves bad word settings with pagination support.
77       *
78       * @param body the search body containing pagination and filter parameters
79       * @return JSON response containing list of bad word configurations
80       */
81      // GET /api/admin/badword/settings
82      // PUT /api/admin/badword/settings
83      @Execute
84      public JsonResponse<ApiResult> settings(final SearchBody body) {
85          validateApi(body, messages -> {});
86          final BadWordPager pager = copyBeanToNewBean(body, BadWordPager.class);
87          final List<BadWord> list = badWordService.getBadWordList(pager);
88          return asJson(
89                  new ApiResult.ApiConfigsResponse<EditBody>().settings(list.stream().map(this::createEditBody).collect(Collectors.toList()))
90                          .total(pager.getAllRecordCount())
91                          .status(ApiResult.Status.OK)
92                          .result());
93      }
94  
95      /**
96       * Retrieves a specific bad word setting by ID.
97       *
98       * @param id the ID of the bad word to retrieve
99       * @return JSON response containing the bad word configuration
100      */
101     // GET /api/admin/badword/{id}
102     @Execute
103     public JsonResponse<ApiResult> get$setting(final String id) {
104 
105         final BadWord entity = badWordService.getBadWord(id).orElseGet(() -> {
106             throwValidationErrorApi(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, id));
107             return null;
108         });
109 
110         final EditBody body = createEditBody(entity);
111         return asJson(new ApiResult.ApiConfigResponse().setting(body).status(ApiResult.Status.OK).result());
112     }
113 
114     // POST /api/admin/badword/setting
115     /**
116      * Creates a new bad word setting.
117      *
118      * @param body the request body containing bad word information
119      * @return JSON response with result status
120      */
121     @Execute
122     public JsonResponse<ApiResult> post$setting(final CreateBody body) {
123         validateApi(body, messages -> {});
124         body.crudMode = CrudMode.CREATE;
125         final BadWord entity = getBadWord(body).orElseGet(() -> {
126             throwValidationErrorApi(messages -> {
127                 messages.addErrorsCrudFailedToCreateInstance(GLOBAL);
128             });
129             return null;
130         });
131         try {
132             badWordService.store(entity);
133             suggestHelper.addBadWord(entity.getSuggestWord(), false);
134         } catch (final Exception e) {
135             logger.warn("Failed to process a request.", e);
136             throwValidationErrorApi(messages -> messages.addErrorsCrudFailedToCreateCrudTable(GLOBAL, buildThrowableMessage(e)));
137         }
138         return asJson(new ApiResult.ApiUpdateResponse().id(entity.getId()).created(true).status(ApiResult.Status.OK).result());
139     }
140 
141     // PUT /api/admin/user/setting
142     /**
143      * Updates an existing bad word setting.
144      *
145      * @param body the request body containing updated bad word information
146      * @return JSON response with result status
147      */
148     @Execute
149     public JsonResponse<ApiResult> put$setting(final EditBody body) {
150         validateApi(body, messages -> {});
151         body.crudMode = CrudMode.EDIT;
152         final BadWord badWord = getBadWord(body).map(entity -> {
153             try {
154                 badWordService.store(entity);
155                 suggestHelper.storeAllBadWords(false);
156             } catch (final Exception e) {
157                 logger.warn("Failed to process a request.", e);
158                 throwValidationErrorApi(messages -> messages.addErrorsCrudFailedToUpdateCrudTable(GLOBAL, buildThrowableMessage(e)));
159             }
160             return entity;
161         }).orElseGet(() -> {
162             throwValidationErrorApi(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, body.id));
163             return null;
164         });
165 
166         return asJson(new ApiUpdateResponse().id(badWord.getId()).created(false).status(Status.OK).result());
167     }
168 
169     /**
170      * Deletes a bad word setting by ID.
171      *
172      * @param id the ID of the bad word to delete
173      * @return JSON response indicating the deletion status
174      */
175     // DELETE /api/admin/badword/setting/{id}
176     @Execute
177     public JsonResponse<ApiResult> delete$setting(final String id) {
178         try {
179             badWordService.getBadWord(id).ifPresent(entity -> {
180                 try {
181                     badWordService.delete(entity);
182                     suggestHelper.deleteBadWord(entity.getSuggestWord());
183                     saveInfo(messages -> messages.addSuccessCrudDeleteCrudTable(GLOBAL));
184                 } catch (final Exception e) {
185                     logger.warn("Failed to process a request.", e);
186                     throwValidationErrorApi(messages -> messages.addErrorsCrudFailedToDeleteCrudTable(GLOBAL, buildThrowableMessage(e)));
187                 }
188             }).orElse(() -> {
189                 throwValidationErrorApi(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, id));
190             });
191         } catch (final Exception e) {
192             logger.warn("Failed to process a request.", e);
193             throwValidationErrorApi(messages -> messages.addErrorsCrudFailedToDeleteCrudTable(GLOBAL, buildThrowableMessage(e)));
194         }
195         return asJson(new ApiResult.ApiUpdateResponse().id(id).created(false).status(ApiResult.Status.OK).result());
196     }
197 
198     // PUT /api/admin/badword/upload
199     /**
200      * Uploads bad words from a CSV file.
201      *
202      * @param body the upload form containing the CSV file
203      * @return JSON response with result status
204      */
205     @Execute
206     public JsonResponse<ApiResult> put$upload(final UploadForm body) {
207         validateApi(body, messages -> {});
208         CommonPoolUtil.execute(() -> {
209             try (Reader reader = new BufferedReader(new InputStreamReader(body.badWordFile.getInputStream(), getCsvEncoding()))) {
210                 badWordService.importCsv(reader);
211                 suggestHelper.storeAllBadWords(false);
212             } catch (final Exception e) {
213                 throw new FessSystemException("Failed to import data.", e);
214             }
215         });
216         return asJson(new ApiResult.ApiResponse().status(ApiResult.Status.OK).result());
217     }
218 
219     /**
220      * Downloads bad word settings as a CSV file.
221      *
222      * @param body the download request body containing download parameters
223      * @return stream response containing the CSV file data
224      */
225     // GET /api/admin/badword/download
226     @Execute
227     public StreamResponse get$download(final DownloadBody body) {
228         validateApi(body, messages -> {});
229         return asStream("badword.csv").contentTypeOctetStream().stream(out -> {
230             final Path tempFile = ComponentUtil.getSystemHelper().createTempFile("fess-badword-", ".csv").toPath();
231             try {
232                 try (Writer writer = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(tempFile), getCsvEncoding()))) {
233                     badWordService.exportCsv(writer);
234                 } catch (final Exception e) {
235                     logger.warn("Failed to process a request.", e);
236                     throwValidationErrorApi(messages -> messages.addErrorsFailedToDownloadBadwordFile(GLOBAL));
237                 }
238                 try (InputStream in = Files.newInputStream(tempFile)) {
239                     out.write(in);
240                 }
241             } finally {
242                 Files.delete(tempFile);
243             }
244         });
245     }
246 
247     /**
248      * Creates an EditBody from a BadWord entity for API responses.
249      *
250      * @param entity the BadWord entity to convert
251      * @return the converted EditBody object
252      */
253     protected EditBody createEditBody(final BadWord entity) {
254         final EditBody body = new EditBody();
255         copyBeanToBean(entity, body, copyOp -> {
256             copyOp.excludeNull();
257         });
258         return body;
259     }
260 
261     private String getCsvEncoding() {
262         return fessConfig.getCsvFileEncoding();
263     }
264 
265 }