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.dict.stopwords;
17  
18  import java.io.File;
19  import java.io.IOException;
20  import java.io.InputStream;
21  
22  import org.apache.logging.log4j.LogManager;
23  import org.apache.logging.log4j.Logger;
24  import org.codelibs.core.beans.util.BeanUtil;
25  import org.codelibs.core.lang.StringUtil;
26  import org.codelibs.fess.Constants;
27  import org.codelibs.fess.annotation.Secured;
28  import org.codelibs.fess.app.pager.StopwordsPager;
29  import org.codelibs.fess.app.service.StopwordsService;
30  import org.codelibs.fess.app.web.CrudMode;
31  import org.codelibs.fess.app.web.admin.dict.AdminDictAction;
32  import org.codelibs.fess.app.web.base.FessAdminAction;
33  import org.codelibs.fess.dict.stopwords.StopwordsItem;
34  import org.codelibs.fess.util.ComponentUtil;
35  import org.codelibs.fess.util.RenderDataUtil;
36  import org.dbflute.optional.OptionalEntity;
37  import org.dbflute.optional.OptionalThing;
38  import org.lastaflute.web.Execute;
39  import org.lastaflute.web.response.ActionResponse;
40  import org.lastaflute.web.response.HtmlResponse;
41  import org.lastaflute.web.response.render.RenderData;
42  import org.lastaflute.web.ruts.process.ActionRuntime;
43  import org.lastaflute.web.validation.VaErrorHook;
44  
45  import jakarta.annotation.Resource;
46  
47  /**
48   * Admin action for Stopwords management.
49   *
50   */
51  public class AdminDictStopwordsAction extends FessAdminAction {
52  
53      /**
54       * Default constructor.
55       */
56      public AdminDictStopwordsAction() {
57          super();
58      }
59  
60      /** The role for this action. */
61      public static final String ROLE = "admin-dict";
62  
63      private static final Logger logger = LogManager.getLogger(AdminDictStopwordsAction.class);
64  
65      // ===================================================================================
66      //                                                                           Attribute
67      //                                                                           =========
68      @Resource
69      private StopwordsService stopwordsService;
70      @Resource
71      private StopwordsPager stopwordsPager;
72  
73      // ===================================================================================
74      //                                                                               Hook
75      //                                                                              ======
76      @Override
77      protected void setupHtmlData(final ActionRuntime runtime) {
78          super.setupHtmlData(runtime);
79          runtime.registerData("helpLink", systemHelper.getHelpLink(fessConfig.getOnlineHelpNameDictStopwords()));
80      }
81  
82      @Override
83      protected String getActionRole() {
84          return ROLE;
85      }
86  
87      // ===================================================================================
88      //                                                                      Search Execute
89      //                                                                      ==============
90      /**
91       * Display the stopwords index page.
92       *
93       * @param form the search form
94       * @return HTML response for the index page
95       */
96      @Execute
97      @Secured({ ROLE, ROLE + VIEW })
98      public HtmlResponse index(final SearchForm form) {
99          validate(form, messages -> {}, this::asDictIndexHtml);
100         stopwordsPager.clear();
101         return asHtml(path_AdminDictStopwords_AdminDictStopwordsJsp).renderWith(data -> {
102             searchPaging(data, form);
103         });
104     }
105 
106     /**
107      * Display the stopwords list with pagination.
108      *
109      * @param pageNumber the page number to display
110      * @param form the search form
111      * @return HTML response for the list page
112      */
113     @Execute
114     @Secured({ ROLE, ROLE + VIEW })
115     public HtmlResponse list(final OptionalThing<Integer> pageNumber, final SearchForm form) {
116         validate(form, messages -> {}, this::asDictIndexHtml);
117         pageNumber.ifPresent(num -> {
118             stopwordsPager.setCurrentPageNumber(pageNumber.get());
119         }).orElse(() -> {
120             stopwordsPager.setCurrentPageNumber(0);
121         });
122         return asHtml(path_AdminDictStopwords_AdminDictStopwordsJsp).renderWith(data -> {
123             searchPaging(data, form);
124         });
125     }
126 
127     /**
128      * Perform search for stopwords.
129      *
130      * @param form the search form containing search criteria
131      * @return HTML response with search results
132      */
133     @Execute
134     @Secured({ ROLE, ROLE + VIEW })
135     public HtmlResponse search(final SearchForm form) {
136         validate(form, messages -> {}, this::asDictIndexHtml);
137         copyBeanToBean(form, stopwordsPager, op -> op.exclude(Constants.PAGER_CONVERSION_RULE));
138         return asHtml(path_AdminDictStopwords_AdminDictStopwordsJsp).renderWith(data -> {
139             searchPaging(data, form);
140         });
141     }
142 
143     /**
144      * Reset search criteria and return to default view.
145      *
146      * @param form the search form to reset
147      * @return HTML response for the reset page
148      */
149     @Execute
150     @Secured({ ROLE, ROLE + VIEW })
151     public HtmlResponse reset(final SearchForm form) {
152         validate(form, messages -> {}, this::asDictIndexHtml);
153         stopwordsPager.clear();
154         return asHtml(path_AdminDictStopwords_AdminDictStopwordsJsp).renderWith(data -> {
155             searchPaging(data, form);
156         });
157     }
158 
159     /**
160      * Set up pagination data for search results.
161      *
162      * @param data the render data to populate
163      * @param form the search form containing pagination parameters
164      */
165     protected void searchPaging(final RenderData data, final SearchForm form) {
166         // page navi
167         RenderDataUtil.register(data, "stopwordsItemItems", stopwordsService.getStopwordsList(form.dictId, stopwordsPager));
168 
169         // restore from pager
170         BeanUtil.copyBeanToBean(stopwordsPager, form, op -> {
171             op.exclude(Constants.PAGER_CONVERSION_RULE);
172         });
173     }
174 
175     // ===================================================================================
176     //                                                                        Edit Execute
177     //                                                                        ============
178     // -----------------------------------------------------
179     //                                            Entry Page
180     //                                            ----------
181     /**
182      * Display the form for creating a new stopwords entry.
183      *
184      * @param dictId the dictionary ID
185      * @return HTML response for the create form
186      */
187     @Execute
188     @Secured({ ROLE })
189     public HtmlResponse createnew(final String dictId) {
190         saveToken();
191         return asHtml(path_AdminDictStopwords_AdminDictStopwordsEditJsp).useForm(CreateForm.class, op -> {
192             op.setup(form -> {
193                 form.initialize();
194                 form.crudMode = CrudMode.CREATE;
195                 form.dictId = dictId;
196             });
197         });
198     }
199 
200     /**
201      * Display the edit form for an existing stopwords entry.
202      *
203      * @param form the edit form containing the entry ID and dictionary ID
204      * @return HTML response for the edit form
205      */
206     @Execute
207     @Secured({ ROLE })
208     public HtmlResponse edit(final EditForm form) {
209         validate(form, messages -> {}, () -> asListHtml(form.dictId));
210         stopwordsService.getStopwordsItem(form.dictId, form.id).ifPresent(entity -> {
211             form.input = entity.getInputValue();
212         }).orElse(() -> {
213             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, form.getDisplayId()),
214                     () -> asListHtml(form.dictId));
215         });
216         saveToken();
217         if (form.crudMode.intValue() == CrudMode.EDIT) {
218             // back
219             form.crudMode = CrudMode.DETAILS;
220             return asDetailsHtml();
221         }
222         form.crudMode = CrudMode.EDIT;
223         return asEditHtml();
224     }
225 
226     // -----------------------------------------------------
227     //                                               Details
228     //                                               -------
229     /**
230      * Display details of a specific stopwords entry.
231      *
232      * @param dictId the dictionary ID
233      * @param crudMode the CRUD mode for the operation
234      * @param id the entry ID
235      * @return HTML response for the details page
236      */
237     @Execute
238     @Secured({ ROLE, ROLE + VIEW })
239     public HtmlResponse details(final String dictId, final int crudMode, final long id) {
240         verifyCrudMode(crudMode, CrudMode.DETAILS, dictId);
241         saveToken();
242         return asDetailsHtml().useForm(EditForm.class, op -> {
243             op.setup(form -> {
244                 stopwordsService.getStopwordsItem(dictId, id).ifPresent(entity -> {
245                     form.input = entity.getInputValue();
246                 }).orElse(() -> {
247                     throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, dictId + ":" + id),
248                             () -> asListHtml(dictId));
249                 });
250                 form.id = id;
251                 form.crudMode = crudMode;
252                 form.dictId = dictId;
253             });
254         });
255     }
256 
257     // -----------------------------------------------------
258     //                                              Download
259     //                                               -------
260     /**
261      * Display the download page for stopwords file.
262      *
263      * @param dictId the dictionary ID
264      * @return HTML response for the download page
265      */
266     @Execute
267     @Secured({ ROLE, ROLE + VIEW })
268     public HtmlResponse downloadpage(final String dictId) {
269         saveToken();
270         return asHtml(path_AdminDictStopwords_AdminDictStopwordsDownloadJsp).useForm(DownloadForm.class, op -> {
271             op.setup(form -> {
272                 form.dictId = dictId;
273             });
274         }).renderWith(data -> {
275             stopwordsService.getStopwordsFile(dictId).ifPresent(file -> {
276                 RenderDataUtil.register(data, "path", file.getPath());
277             }).orElse(() -> {
278                 throwValidationError(messages -> messages.addErrorsFailedToDownloadStopwordsFile(GLOBAL), this::asDictIndexHtml);
279             });
280         });
281     }
282 
283     /**
284      * Download the stopwords file.
285      *
286      * @param form the download form containing dictionary ID
287      * @return action response with the file download stream
288      */
289     @Execute
290     @Secured({ ROLE, ROLE + VIEW })
291     public ActionResponse download(final DownloadForm form) {
292         validate(form, messages -> {}, () -> downloadpage(form.dictId));
293         verifyTokenKeep(() -> downloadpage(form.dictId));
294         return stopwordsService.getStopwordsFile(form.dictId)
295                 .map(file -> asStream(new File(file.getPath()).getName()).contentTypeOctetStream().stream(out -> {
296                     file.writeOut(out);
297                 }))
298                 .orElseGet(() -> {
299                     throwValidationError(messages -> messages.addErrorsFailedToDownloadStopwordsFile(GLOBAL),
300                             () -> downloadpage(form.dictId));
301                     return null;
302                 });
303     }
304 
305     // -----------------------------------------------------
306     //                                                Upload
307     //                                               -------
308     /**
309      * Display the upload page for stopwords file.
310      *
311      * @param dictId the dictionary ID
312      * @return HTML response for the upload page
313      */
314     @Execute
315     @Secured({ ROLE })
316     public HtmlResponse uploadpage(final String dictId) {
317         saveToken();
318         return asHtml(path_AdminDictStopwords_AdminDictStopwordsUploadJsp).useForm(UploadForm.class, op -> {
319             op.setup(form -> {
320                 form.dictId = dictId;
321             });
322         }).renderWith(data -> {
323             stopwordsService.getStopwordsFile(dictId).ifPresent(file -> {
324                 RenderDataUtil.register(data, "path", file.getPath());
325             }).orElse(() -> {
326                 throwValidationError(messages -> messages.addErrorsFailedToDownloadStopwordsFile(GLOBAL), this::asDictIndexHtml);
327             });
328         });
329     }
330 
331     /**
332      * Upload a stopwords file.
333      *
334      * @param form the upload form containing the file and dictionary ID
335      * @return HTML response redirecting to the list page after successful upload
336      */
337     @Execute
338     @Secured({ ROLE })
339     public HtmlResponse upload(final UploadForm form) {
340         validate(form, messages -> {}, () -> uploadpage(form.dictId));
341         verifyToken(() -> uploadpage(form.dictId));
342         return stopwordsService.getStopwordsFile(form.dictId).map(file -> {
343             try (InputStream inputStream = form.stopwordsFile.getInputStream()) {
344                 file.update(inputStream);
345             } catch (final IOException e) {
346                 logger.warn("Failed to process a request.", e);
347                 throwValidationError(messages -> messages.addErrorsFailedToUploadStopwordsFile(GLOBAL),
348                         () -> redirectWith(getClass(), moreUrl("uploadpage/" + form.dictId)));
349             }
350             saveInfo(messages -> messages.addSuccessUploadStopwordsFile(GLOBAL));
351             return redirectWith(getClass(), moreUrl("list/1").params("dictId", form.dictId));
352         }).orElseGet(() -> {
353             throwValidationError(messages -> messages.addErrorsFailedToUploadStopwordsFile(GLOBAL), () -> uploadpage(form.dictId));
354             return null;
355         });
356 
357     }
358 
359     // -----------------------------------------------------
360     //                                         Actually Crud
361     //                                         -------------
362     /**
363      * Create a new stopwords entry.
364      *
365      * @param form the create form containing the new entry data
366      * @return HTML response redirecting to the list page after successful creation
367      */
368     @Execute
369     @Secured({ ROLE })
370     public HtmlResponse create(final CreateForm form) {
371         verifyCrudMode(form.crudMode, CrudMode.CREATE, form.dictId);
372         validate(form, messages -> {}, this::asEditHtml);
373         verifyToken(this::asEditHtml);
374         createStopwordsItem(form, this::asEditHtml).ifPresent(entity -> {
375             stopwordsService.store(form.dictId, entity);
376             saveInfo(messages -> messages.addSuccessCrudCreateCrudTable(GLOBAL));
377         }).orElse(() -> throwValidationError(messages -> messages.addErrorsCrudFailedToCreateInstance(GLOBAL), this::asEditHtml));
378         return redirectWith(getClass(), moreUrl("list/1").params("dictId", form.dictId));
379     }
380 
381     /**
382      * Update an existing stopwords entry.
383      *
384      * @param form the edit form containing the updated entry data
385      * @return HTML response redirecting to the list page after successful update
386      */
387     @Execute
388     @Secured({ ROLE })
389     public HtmlResponse update(final EditForm form) {
390         verifyCrudMode(form.crudMode, CrudMode.EDIT, form.dictId);
391         validate(form, messages -> {}, this::asEditHtml);
392         verifyToken(this::asEditHtml);
393         createStopwordsItem(form, this::asEditHtml).ifPresent(entity -> {
394             stopwordsService.store(form.dictId, entity);
395             saveInfo(messages -> messages.addSuccessCrudUpdateCrudTable(GLOBAL));
396         })
397                 .orElse(() -> throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, form.getDisplayId()),
398                         this::asEditHtml));
399         return redirectWith(getClass(), moreUrl("list/1").params("dictId", form.dictId));
400     }
401 
402     /**
403      * Delete a stopwords entry.
404      *
405      * @param form the edit form containing the entry ID to delete
406      * @return HTML response redirecting to the list page after successful deletion
407      */
408     @Execute
409     @Secured({ ROLE })
410     public HtmlResponse delete(final EditForm form) {
411         verifyCrudMode(form.crudMode, CrudMode.DETAILS, form.dictId);
412         validate(form, messages -> {}, this::asDetailsHtml);
413         verifyToken(this::asDetailsHtml);
414         stopwordsService.getStopwordsItem(form.dictId, form.id).ifPresent(entity -> {
415             stopwordsService.delete(form.dictId, entity);
416             saveInfo(messages -> messages.addSuccessCrudDeleteCrudTable(GLOBAL));
417         }).orElse(() -> {
418             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, form.getDisplayId()), this::asDetailsHtml);
419         });
420         return redirectWith(getClass(), moreUrl("list/1").params("dictId", form.dictId));
421     }
422 
423     //===================================================================================
424     //                                                                        Assist Logic
425     //                                                                        ============
426 
427     private static OptionalEntity<StopwordsItem> getEntity(final CreateForm form) {
428         switch (form.crudMode) {
429         case CrudMode.CREATE:
430             final StopwordsItem entity = new StopwordsItem(0, StringUtil.EMPTY);
431             return OptionalEntity.of(entity);
432         case CrudMode.EDIT:
433             if (form instanceof EditForm) {
434                 return ComponentUtil.getComponent(StopwordsService.class).getStopwordsItem(form.dictId, ((EditForm) form).id);
435             }
436             break;
437         default:
438             break;
439         }
440         return OptionalEntity.empty();
441     }
442 
443     /**
444      * Create a StopwordsItem from the form data.
445      *
446      * @param form the create form containing the item data
447      * @param hook the validation error hook
448      * @return optional entity containing the created stopwords item
449      */
450     public static OptionalEntity<StopwordsItem> createStopwordsItem(final CreateForm form, final VaErrorHook hook) {
451         return getEntity(form).map(entity -> {
452             final String newInput = form.input;
453             validateStopwordsString(newInput, "input", hook);
454             entity.setNewInput(newInput);
455             return entity;
456         });
457     }
458 
459     // ===================================================================================
460     //                                                                        Small Helper
461     //                                                                        ============
462     /**
463      * Verify that the CRUD mode matches the expected mode.
464      *
465      * @param crudMode the actual CRUD mode
466      * @param expectedMode the expected CRUD mode
467      * @param dictId the dictionary ID for error redirection
468      */
469     protected void verifyCrudMode(final int crudMode, final int expectedMode, final String dictId) {
470         if (crudMode != expectedMode) {
471             throwValidationError(messages -> {
472                 messages.addErrorsCrudInvalidMode(GLOBAL, String.valueOf(expectedMode), String.valueOf(crudMode));
473             }, () -> asListHtml(dictId));
474         }
475     }
476 
477     private static void validateStopwordsString(final String values, final String propertyName, final VaErrorHook hook) {
478         // TODO validation
479     }
480 
481     // ===================================================================================
482     //                                                                              JSP
483     //                                                                           =========
484 
485     /**
486      * Redirect to the dictionary index page.
487      *
488      * @return HTML response redirecting to the dictionary index
489      */
490     protected HtmlResponse asDictIndexHtml() {
491         return redirect(AdminDictAction.class);
492     }
493 
494     private HtmlResponse asListHtml(final String dictId) {
495         return asHtml(path_AdminDictStopwords_AdminDictStopwordsJsp).renderWith(data -> {
496             RenderDataUtil.register(data, "stopwordsItemItems", stopwordsService.getStopwordsList(dictId, stopwordsPager));
497         }).useForm(SearchForm.class, setup -> {
498             setup.setup(form -> {
499                 copyBeanToBean(stopwordsPager, form, op -> op.include("id"));
500             });
501         });
502     }
503 
504     private HtmlResponse asEditHtml() {
505         return asHtml(path_AdminDictStopwords_AdminDictStopwordsEditJsp);
506     }
507 
508     private HtmlResponse asDetailsHtml() {
509         return asHtml(path_AdminDictStopwords_AdminDictStopwordsDetailsJsp);
510     }
511 
512 }