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.admin.badword;
17  
18  import java.io.BufferedReader;
19  import java.io.BufferedWriter;
20  import java.io.InputStream;
21  import java.io.InputStreamReader;
22  import java.io.OutputStreamWriter;
23  import java.io.Reader;
24  import java.io.Writer;
25  import java.nio.file.Files;
26  import java.nio.file.Path;
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.concurrent.CommonPoolUtil;
32  import org.codelibs.fess.Constants;
33  import org.codelibs.fess.annotation.Secured;
34  import org.codelibs.fess.app.pager.BadWordPager;
35  import org.codelibs.fess.app.service.BadWordService;
36  import org.codelibs.fess.app.web.CrudMode;
37  import org.codelibs.fess.app.web.base.FessAdminAction;
38  import org.codelibs.fess.exception.FessSystemException;
39  import org.codelibs.fess.helper.SuggestHelper;
40  import org.codelibs.fess.helper.SystemHelper;
41  import org.codelibs.fess.opensearch.config.exentity.BadWord;
42  import org.codelibs.fess.util.ComponentUtil;
43  import org.codelibs.fess.util.RenderDataUtil;
44  import org.dbflute.optional.OptionalEntity;
45  import org.dbflute.optional.OptionalThing;
46  import org.lastaflute.web.Execute;
47  import org.lastaflute.web.response.ActionResponse;
48  import org.lastaflute.web.response.HtmlResponse;
49  import org.lastaflute.web.response.render.RenderData;
50  import org.lastaflute.web.ruts.process.ActionRuntime;
51  
52  import jakarta.annotation.Resource;
53  
54  /**
55   * Admin action for Bad Word management.
56   *
57   */
58  public class AdminBadwordAction extends FessAdminAction {
59  
60      /**
61       * Default constructor.
62       */
63      public AdminBadwordAction() {
64          super();
65      }
66  
67      /**
68       * The role for this action.
69       */
70      public static final String ROLE = "admin-badword";
71  
72      private static final Logger logger = LogManager.getLogger(AdminBadwordAction.class);
73  
74      // ===================================================================================
75      //                                                                           Attribute
76      //                                                                           =========
77      @Resource
78      private BadWordService badWordService;
79      @Resource
80      private BadWordPager badWordPager;
81  
82      /**
83       * The suggest helper.
84       */
85      @Resource
86      protected SuggestHelper suggestHelper;
87  
88      // ===================================================================================
89      //                                                                               Hook
90      //                                                                              ======
91      @Override
92      protected void setupHtmlData(final ActionRuntime runtime) {
93          super.setupHtmlData(runtime);
94          runtime.registerData("helpLink", systemHelper.getHelpLink(fessConfig.getOnlineHelpNameBadword()));
95      }
96  
97      @Override
98      protected String getActionRole() {
99          return ROLE;
100     }
101 
102     // ===================================================================================
103     //                                                                      Search Execute
104     //                                                                      ==============
105     /**
106      * Show the index page.
107      * @return The HTML response.
108      */
109     @Execute
110     @Secured({ ROLE, ROLE + VIEW })
111     public HtmlResponse index() {
112         return asListHtml();
113     }
114 
115     /**
116      * Show the list page.
117      * @param pageNumber The page number.
118      * @param form The search form.
119      * @return The HTML response.
120      */
121     @Execute
122     @Secured({ ROLE, ROLE + VIEW })
123     public HtmlResponse list(final OptionalThing<Integer> pageNumber, final SearchForm form) {
124         pageNumber.ifPresent(num -> {
125             badWordPager.setCurrentPageNumber(pageNumber.get());
126         }).orElse(() -> {
127             badWordPager.setCurrentPageNumber(0);
128         });
129         return asHtml(path_AdminBadword_AdminBadwordJsp).renderWith(data -> {
130             searchPaging(data, form);
131         });
132     }
133 
134     /**
135      * Search the bad words.
136      * @param form The search form.
137      * @return The HTML response.
138      */
139     @Execute
140     @Secured({ ROLE, ROLE + VIEW })
141     public HtmlResponse search(final SearchForm form) {
142         copyBeanToBean(form, badWordPager, op -> op.exclude(Constants.PAGER_CONVERSION_RULE));
143         return asHtml(path_AdminBadword_AdminBadwordJsp).renderWith(data -> {
144             searchPaging(data, form);
145         });
146     }
147 
148     /**
149      * Reset the search form.
150      * @param form The search form.
151      * @return The HTML response.
152      */
153     @Execute
154     @Secured({ ROLE, ROLE + VIEW })
155     public HtmlResponse reset(final SearchForm form) {
156         badWordPager.clear();
157         return asHtml(path_AdminBadword_AdminBadwordJsp).renderWith(data -> {
158             searchPaging(data, form);
159         });
160     }
161 
162     /**
163      * Search with paging.
164      * @param data The render data.
165      * @param form The search form.
166      */
167     protected void searchPaging(final RenderData data, final SearchForm form) {
168         RenderDataUtil.register(data, "badWordItems", badWordService.getBadWordList(badWordPager)); // page navi
169 
170         // restore from pager
171         copyBeanToBean(badWordPager, form, op -> op.include("id"));
172     }
173 
174     // ===================================================================================
175     //                                                                        Edit Execute
176     //                                                                        ============
177     // -----------------------------------------------------
178     //                                            Entry Page
179     //                                            ----------
180     /**
181      * Show the create new page.
182      * @return The HTML response.
183      */
184     @Execute
185     @Secured({ ROLE })
186     public HtmlResponse createnew() {
187         saveToken();
188         return asEditHtml().useForm(CreateForm.class, op -> {
189             op.setup(form -> {
190                 form.initialize();
191                 form.crudMode = CrudMode.CREATE;
192             });
193         });
194     }
195 
196     /**
197      * Show the edit page.
198      * @param form The edit form.
199      * @return The HTML response.
200      */
201     @Execute
202     @Secured({ ROLE })
203     public HtmlResponse edit(final EditForm form) {
204         validate(form, messages -> {}, this::asListHtml);
205         final String id = form.id;
206         badWordService.getBadWord(id).ifPresent(entity -> {
207             copyBeanToBean(entity, form, op -> {});
208         }).orElse(() -> {
209             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, id), this::asListHtml);
210         });
211         saveToken();
212         if (form.crudMode.intValue() == CrudMode.EDIT) {
213             // back
214             form.crudMode = CrudMode.DETAILS;
215             return asDetailsHtml();
216         }
217         form.crudMode = CrudMode.EDIT;
218         return asEditHtml();
219     }
220 
221     // -----------------------------------------------------
222     //                                               Details
223     //                                               -------
224     /**
225      * Show the details page.
226      * @param crudMode The CRUD mode.
227      * @param id The ID.
228      * @return The HTML response.
229      */
230     @Execute
231     @Secured({ ROLE, ROLE + VIEW })
232     public HtmlResponse details(final int crudMode, final String id) {
233         verifyCrudMode(crudMode, CrudMode.DETAILS, this::asListHtml);
234         saveToken();
235         return asDetailsHtml().useForm(EditForm.class, op -> {
236             op.setup(form -> {
237                 badWordService.getBadWord(id).ifPresent(entity -> {
238                     copyBeanToBean(entity, form, copyOp -> {
239                         copyOp.excludeNull();
240                     });
241                     form.crudMode = crudMode;
242                 }).orElse(() -> {
243                     throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, id), this::asListHtml);
244                 });
245             });
246         });
247     }
248 
249     // -----------------------------------------------------
250     //                                              Download
251     //                                               -------
252     /**
253      * Show the download page.
254      * @return The HTML response.
255      */
256     @Execute
257     @Secured({ ROLE, ROLE + VIEW })
258     public HtmlResponse downloadpage() {
259         saveToken();
260         return asDownloadHtml();
261     }
262 
263     /**
264      * Download the bad words.
265      * @param form The download form.
266      * @return The action response.
267      */
268     @Execute
269     @Secured({ ROLE, ROLE + VIEW })
270     public ActionResponse download(final DownloadForm form) {
271         verifyToken(this::asDownloadHtml);
272 
273         return asStream("badword.csv").contentTypeOctetStream().stream(out -> {
274             final Path tempFile = ComponentUtil.getSystemHelper().createTempFile("fess-badword-", ".csv").toPath();
275             try {
276                 try (Writer writer = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(tempFile), getCsvEncoding()))) {
277                     badWordService.exportCsv(writer);
278                 } catch (final Exception e) {
279                     logger.warn("Failed to process a request.", e);
280                     throwValidationError(messages -> messages.addErrorsFailedToDownloadBadwordFile(GLOBAL), this::asDownloadHtml);
281                 }
282                 try (InputStream in = Files.newInputStream(tempFile)) {
283                     out.write(in);
284                 }
285             } finally {
286                 Files.delete(tempFile);
287             }
288         });
289     }
290 
291     // -----------------------------------------------------
292     //                                                Upload
293     //                                               -------
294     /**
295      * Show the upload page.
296      * @return The HTML response.
297      */
298     @Execute
299     @Secured({ ROLE })
300     public HtmlResponse uploadpage() {
301         saveToken();
302         return asUploadHtml();
303     }
304 
305     // -----------------------------------------------------
306     //                                         Actually Crud
307     //                                         -------------
308     /**
309      * Create a bad word.
310      * @param form The create form.
311      * @return The HTML response.
312      */
313     @Execute
314     @Secured({ ROLE })
315     public HtmlResponse create(final CreateForm form) {
316         verifyCrudMode(form.crudMode, CrudMode.CREATE, this::asListHtml);
317         validate(form, messages -> {}, this::asEditHtml);
318         verifyToken(this::asEditHtml);
319         getBadWord(form).ifPresent(entity -> {
320             try {
321                 badWordService.store(entity);
322                 suggestHelper.addBadWord(entity.getSuggestWord(), false);
323                 saveInfo(messages -> messages.addSuccessCrudCreateCrudTable(GLOBAL));
324             } catch (final Exception e) {
325                 logger.warn("Failed to process a request.", e);
326                 throwValidationError(messages -> messages.addErrorsCrudFailedToCreateCrudTable(GLOBAL, buildThrowableMessage(e)),
327                         this::asEditHtml);
328             }
329         }).orElse(() -> {
330             throwValidationError(messages -> messages.addErrorsCrudFailedToCreateInstance(GLOBAL), this::asEditHtml);
331         });
332         return redirect(getClass());
333     }
334 
335     /**
336      * Update a bad word.
337      * @param form The edit form.
338      * @return The HTML response.
339      */
340     @Execute
341     @Secured({ ROLE })
342     public HtmlResponse update(final EditForm form) {
343         verifyCrudMode(form.crudMode, CrudMode.EDIT, this::asListHtml);
344         validate(form, messages -> {}, this::asEditHtml);
345         verifyToken(this::asEditHtml);
346         getBadWord(form).ifPresent(entity -> {
347             try {
348                 badWordService.store(entity);
349                 suggestHelper.storeAllBadWords(false);
350                 saveInfo(messages -> messages.addSuccessCrudUpdateCrudTable(GLOBAL));
351             } catch (final Exception e) {
352                 logger.warn("Failed to process a request.", e);
353                 throwValidationError(messages -> messages.addErrorsCrudFailedToUpdateCrudTable(GLOBAL, buildThrowableMessage(e)),
354                         this::asEditHtml);
355             }
356         }).orElse(() -> {
357             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, form.id), this::asEditHtml);
358         });
359         return redirect(getClass());
360     }
361 
362     /**
363      * Delete a bad word.
364      * @param form The edit form.
365      * @return The HTML response.
366      */
367     @Execute
368     @Secured({ ROLE })
369     public HtmlResponse delete(final EditForm form) {
370         verifyCrudMode(form.crudMode, CrudMode.DETAILS, this::asListHtml);
371         validate(form, messages -> {}, this::asDetailsHtml);
372         verifyToken(this::asDetailsHtml);
373         final String id = form.id;
374         badWordService.getBadWord(id).ifPresent(entity -> {
375             try {
376                 badWordService.delete(entity);
377                 suggestHelper.deleteBadWord(entity.getSuggestWord());
378                 saveInfo(messages -> messages.addSuccessCrudDeleteCrudTable(GLOBAL));
379             } catch (final Exception e) {
380                 logger.warn("Failed to process a request.", e);
381                 throwValidationError(messages -> messages.addErrorsCrudFailedToDeleteCrudTable(GLOBAL, buildThrowableMessage(e)),
382                         this::asDetailsHtml);
383             }
384         }).orElse(() -> {
385             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, id), this::asDetailsHtml);
386         });
387         return redirect(getClass());
388     }
389 
390     /**
391      * Upload the bad words.
392      * @param form The upload form.
393      * @return The HTML response.
394      */
395     @Execute
396     @Secured({ ROLE })
397     public HtmlResponse upload(final UploadForm form) {
398         validate(form, messages -> {}, this::asUploadHtml);
399         verifyToken(this::asUploadHtml);
400         CommonPoolUtil.execute(() -> {
401             try (Reader reader = new BufferedReader(new InputStreamReader(form.badWordFile.getInputStream(), getCsvEncoding()))) {
402                 badWordService.importCsv(reader);
403                 suggestHelper.storeAllBadWords(false);
404             } catch (final Exception e) {
405                 throw new FessSystemException("Failed to import bad word data: fileName=" + form.badWordFile.getFileName(), e);
406             }
407         });
408         saveInfo(messages -> messages.addSuccessUploadBadWord(GLOBAL));
409         return redirect(getClass());
410     }
411 
412     // ===================================================================================
413     //                                                                        Assist Logic
414     //                                                                        ============
415     private static OptionalEntity<BadWord> getEntity(final CreateForm form, final String username, final long currentTime) {
416         switch (form.crudMode) {
417         case CrudMode.CREATE:
418             return OptionalEntity.of(new BadWord()).map(entity -> {
419                 entity.setCreatedBy(username);
420                 entity.setCreatedTime(currentTime);
421                 return entity;
422             });
423         case CrudMode.EDIT:
424             if (form instanceof EditForm) {
425                 return ComponentUtil.getComponent(BadWordService.class).getBadWord(((EditForm) form).id);
426             }
427             break;
428         default:
429             break;
430         }
431         return OptionalEntity.empty();
432     }
433 
434     /**
435      * Get the bad word.
436      * @param form The create form.
437      * @return The bad word.
438      */
439     public static OptionalEntity<BadWord> getBadWord(final CreateForm form) {
440         final SystemHelper systemHelper = ComponentUtil.getSystemHelper();
441         final String username = systemHelper.getUsername();
442         final long currentTime = systemHelper.getCurrentTimeAsLong();
443         return getEntity(form, username, currentTime).map(entity -> {
444             entity.setUpdatedBy(username);
445             entity.setUpdatedTime(currentTime);
446             BeanUtil.copyBeanToBean(form, entity, op -> op.exclude(Constants.COMMON_CONVERSION_RULE));
447             return entity;
448         });
449     }
450 
451     // ===================================================================================
452     //                                                                        Small Helper
453     //                                                                        ============
454     private String getCsvEncoding() {
455         return fessConfig.getCsvFileEncoding();
456     }
457 
458     // ===================================================================================
459     //                                                                              JSP
460     //                                                                           =========
461     private HtmlResponse asListHtml() {
462         return asHtml(path_AdminBadword_AdminBadwordJsp).renderWith(data -> {
463             RenderDataUtil.register(data, "badWordItems", badWordService.getBadWordList(badWordPager));
464         }).useForm(SearchForm.class, setup -> {
465             setup.setup(form -> {
466                 copyBeanToBean(badWordPager, form, op -> op.include("id"));
467             });
468         });
469     }
470 
471     private HtmlResponse asEditHtml() {
472         return asHtml(path_AdminBadword_AdminBadwordEditJsp);
473     }
474 
475     private HtmlResponse asDetailsHtml() {
476         return asHtml(path_AdminBadword_AdminBadwordDetailsJsp);
477     }
478 
479     private HtmlResponse asUploadHtml() {
480         return asHtml(path_AdminBadword_AdminBadwordUploadJsp).useForm(UploadForm.class);
481     }
482 
483     private HtmlResponse asDownloadHtml() {
484         return asHtml(path_AdminBadword_AdminBadwordDownloadJsp).useForm(DownloadForm.class);
485     }
486 
487 }