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.stemmeroverride;
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.StemmerOverridePager;
29  import org.codelibs.fess.app.service.StemmerOverrideService;
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.app.web.base.FessBaseAction;
34  import org.codelibs.fess.dict.stemmeroverride.StemmerOverrideItem;
35  import org.codelibs.fess.util.ComponentUtil;
36  import org.codelibs.fess.util.RenderDataUtil;
37  import org.dbflute.optional.OptionalEntity;
38  import org.dbflute.optional.OptionalThing;
39  import org.lastaflute.web.Execute;
40  import org.lastaflute.web.response.ActionResponse;
41  import org.lastaflute.web.response.HtmlResponse;
42  import org.lastaflute.web.response.render.RenderData;
43  import org.lastaflute.web.ruts.process.ActionRuntime;
44  import org.lastaflute.web.validation.VaErrorHook;
45  import org.lastaflute.web.validation.exception.ValidationErrorException;
46  
47  import jakarta.annotation.Resource;
48  
49  /**
50   * Admin action for Stemmer Override management.
51   *
52   */
53  public class AdminDictStemmeroverrideAction extends FessAdminAction {
54  
55      /**
56       * Default constructor.
57       */
58      public AdminDictStemmeroverrideAction() {
59          super();
60      }
61  
62      /**
63       * The role for this action.
64       */
65      public static final String ROLE = "admin-dict";
66  
67      private static final Logger logger = LogManager.getLogger(AdminDictStemmeroverrideAction.class);
68  
69      // ===================================================================================
70      //                                                                           Attribute
71      //                                                                           =========
72      @Resource
73      private StemmerOverrideService stemmerOverrideService;
74      @Resource
75      private StemmerOverridePager stemmerOverridePager;
76  
77      // ===================================================================================
78      //                                                                               Hook
79      //                                                                              ======
80      @Override
81      protected void setupHtmlData(final ActionRuntime runtime) {
82          super.setupHtmlData(runtime);
83          runtime.registerData("helpLink", systemHelper.getHelpLink(fessConfig.getOnlineHelpNameDictStemmeroverride()));
84      }
85  
86      @Override
87      protected String getActionRole() {
88          return ROLE;
89      }
90  
91      // ===================================================================================
92      //                                                                      Search Execute
93      //                                                                      ==============
94      /**
95       * Display the main index page for stemmer override dictionary management.
96       * Clears the pager and shows the initial search form.
97       *
98       * @param form The search form containing filter criteria
99       * @return HTML response for the stemmer override index page
100      */
101     @Execute
102     @Secured({ ROLE, ROLE + VIEW })
103     public HtmlResponse index(final SearchForm form) {
104         validate(form, messages -> {}, this::asDictIndexHtml);
105         stemmerOverridePager.clear();
106         return asHtml(path_AdminDictStemmeroverride_AdminDictStemmeroverrideJsp).renderWith(data -> {
107             searchPaging(data, form);
108         });
109     }
110 
111     /**
112      * Display a paginated list of stemmer override items.
113      * Sets the current page number and shows the list with pagination.
114      *
115      * @param pageNumber Optional page number to display (0-based)
116      * @param form The search form containing filter criteria
117      * @return HTML response showing the stemmer override list
118      */
119     @Execute
120     @Secured({ ROLE, ROLE + VIEW })
121     public HtmlResponse list(final OptionalThing<Integer> pageNumber, final SearchForm form) {
122         validate(form, messages -> {}, this::asDictIndexHtml);
123         pageNumber.ifPresent(num -> {
124             stemmerOverridePager.setCurrentPageNumber(pageNumber.get());
125         }).orElse(() -> {
126             stemmerOverridePager.setCurrentPageNumber(0);
127         });
128         return asHtml(path_AdminDictStemmeroverride_AdminDictStemmeroverrideJsp).renderWith(data -> {
129             searchPaging(data, form);
130         });
131     }
132 
133     /**
134      * Perform a search operation for stemmer override items.
135      * Updates the pager with search criteria and displays filtered results.
136      *
137      * @param form The search form containing search criteria
138      * @return HTML response showing search results
139      */
140     @Execute
141     @Secured({ ROLE, ROLE + VIEW })
142     public HtmlResponse search(final SearchForm form) {
143         validate(form, messages -> {}, this::asDictIndexHtml);
144         copyBeanToBean(form, stemmerOverridePager, op -> op.exclude(Constants.PAGER_CONVERSION_RULE));
145         return asHtml(path_AdminDictStemmeroverride_AdminDictStemmeroverrideJsp).renderWith(data -> {
146             searchPaging(data, form);
147         });
148     }
149 
150     /**
151      * Reset the search criteria and pager to default state.
152      * Clears all filters and returns to the initial page view.
153      *
154      * @param form The search form to reset
155      * @return HTML response showing the reset stemmer override list
156      */
157     @Execute
158     @Secured({ ROLE, ROLE + VIEW })
159     public HtmlResponse reset(final SearchForm form) {
160         validate(form, messages -> {}, this::asDictIndexHtml);
161         stemmerOverridePager.clear();
162         return asHtml(path_AdminDictStemmeroverride_AdminDictStemmeroverrideJsp).renderWith(data -> {
163             searchPaging(data, form);
164         });
165     }
166 
167     /**
168      * Populate render data with stemmer override items for pagination display.
169      * Retrieves items based on search criteria and restores form data from pager.
170      *
171      * @param data The render data to populate
172      * @param form The search form containing criteria
173      */
174     protected void searchPaging(final RenderData data, final SearchForm form) {
175         // page navi
176         RenderDataUtil.register(data, "stemmerOverrideItemItems",
177                 stemmerOverrideService.getStemmerOverrideList(form.dictId, stemmerOverridePager));
178 
179         // restore from pager
180         BeanUtil.copyBeanToBean(stemmerOverridePager, form, op -> {
181             op.exclude(Constants.PAGER_CONVERSION_RULE);
182         });
183     }
184 
185     // ===================================================================================
186     //                                                                        Edit Execute
187     //                                                                        ============
188     // -----------------------------------------------------
189     //                                            Entry Page
190     //                                            ----------
191     /**
192      * Show the create new page.
193      * @param dictId The dictionary ID.
194      * @return The HTML response.
195      */
196     @Execute
197     @Secured({ ROLE })
198     public HtmlResponse createnew(final String dictId) {
199         saveToken();
200         return asHtml(path_AdminDictStemmeroverride_AdminDictStemmeroverrideEditJsp).useForm(CreateForm.class, op -> {
201             op.setup(form -> {
202                 form.initialize();
203                 form.crudMode = CrudMode.CREATE;
204                 form.dictId = dictId;
205             });
206         });
207     }
208 
209     /**
210      * Display the edit form for an existing stemmer override item.
211      * Loads the item data and switches to edit mode or details view based on current state.
212      *
213      * @param form The edit form containing item ID and CRUD mode
214      * @return HTML response for the edit page or details page
215      */
216     @Execute
217     @Secured({ ROLE })
218     public HtmlResponse edit(final EditForm form) {
219         validate(form, messages -> {}, () -> asListHtml(form.dictId));
220         stemmerOverrideService.getStemmerOverrideItem(form.dictId, form.id).ifPresent(entity -> {
221             form.input = entity.getInput();
222             form.output = entity.getOutput();
223         }).orElse(() -> {
224             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, form.getDisplayId()),
225                     () -> asListHtml(form.dictId));
226         });
227         saveToken();
228         if (form.crudMode.intValue() == CrudMode.EDIT) {
229             // back
230             form.crudMode = CrudMode.DETAILS;
231             return asDetailsHtml();
232         }
233         form.crudMode = CrudMode.EDIT;
234         return asEditHtml();
235     }
236 
237     // -----------------------------------------------------
238     //                                               Details
239     //                                               -------
240     /**
241      * Display detailed view of a specific stemmer override item.
242      * Shows read-only details of the selected item.
243      *
244      * @param dictId The dictionary ID
245      * @param crudMode The CRUD mode (should be DETAILS)
246      * @param id The ID of the stemmer override item to display
247      * @return HTML response showing item details
248      */
249     @Execute
250     @Secured({ ROLE, ROLE + VIEW })
251     public HtmlResponse details(final String dictId, final int crudMode, final long id) {
252         verifyCrudMode(crudMode, CrudMode.DETAILS, dictId);
253         saveToken();
254         return asDetailsHtml().useForm(EditForm.class, op -> {
255             op.setup(form -> {
256                 stemmerOverrideService.getStemmerOverrideItem(dictId, id).ifPresent(entity -> {
257                     form.input = entity.getInput();
258                     form.output = entity.getOutput();
259                 }).orElse(() -> {
260                     throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, dictId + ":" + id),
261                             () -> asListHtml(dictId));
262                 });
263                 form.id = id;
264                 form.crudMode = crudMode;
265                 form.dictId = dictId;
266             });
267         });
268     }
269 
270     // -----------------------------------------------------
271     //                                              Download
272     //                                               -------
273     /**
274      * Display the download page for stemmer override dictionary file.
275      * Shows the file path and provides download interface.
276      *
277      * @param dictId The dictionary ID to download
278      * @return HTML response for the download page
279      */
280     @Execute
281     @Secured({ ROLE, ROLE + VIEW })
282     public HtmlResponse downloadpage(final String dictId) {
283         saveToken();
284         return asHtml(path_AdminDictStemmeroverride_AdminDictStemmeroverrideDownloadJsp).useForm(DownloadForm.class, op -> {
285             op.setup(form -> {
286                 form.dictId = dictId;
287             });
288         }).renderWith(data -> {
289             stemmerOverrideService.getStemmerOverrideFile(dictId).ifPresent(file -> {
290                 RenderDataUtil.register(data, "path", file.getPath());
291             }).orElse(() -> {
292                 throwValidationError(messages -> messages.addErrorsFailedToDownloadStemmeroverrideFile(GLOBAL), this::asDictIndexHtml);
293             });
294         });
295     }
296 
297     /**
298      * Download the stemmer override dictionary file.
299      * Streams the dictionary file as an octet-stream download.
300      *
301      * @param form The download form containing dictionary ID
302      * @return Action response with file stream for download
303      */
304     @Execute
305     @Secured({ ROLE, ROLE + VIEW })
306     public ActionResponse download(final DownloadForm form) {
307         validate(form, messages -> {}, () -> downloadpage(form.dictId));
308         verifyTokenKeep(() -> downloadpage(form.dictId));
309         return stemmerOverrideService.getStemmerOverrideFile(form.dictId)
310                 .map(file -> asStream(new File(file.getPath()).getName()).contentTypeOctetStream().stream(out -> {
311                     file.writeOut(out);
312                 }))
313                 .orElseGet(() -> {
314                     throwValidationError(messages -> messages.addErrorsFailedToDownloadStemmeroverrideFile(GLOBAL),
315                             () -> downloadpage(form.dictId));
316                     return null;
317                 });
318     }
319 
320     // -----------------------------------------------------
321     //                                                Upload
322     //                                               -------
323     /**
324      * Display the upload page for stemmer override dictionary file.
325      * Shows the current file path and provides upload interface.
326      *
327      * @param dictId The dictionary ID to upload to
328      * @return HTML response for the upload page
329      */
330     @Execute
331     @Secured({ ROLE })
332     public HtmlResponse uploadpage(final String dictId) {
333         saveToken();
334         return asHtml(path_AdminDictStemmeroverride_AdminDictStemmeroverrideUploadJsp).useForm(UploadForm.class, op -> {
335             op.setup(form -> {
336                 form.dictId = dictId;
337             });
338         }).renderWith(data -> {
339             stemmerOverrideService.getStemmerOverrideFile(dictId).ifPresent(file -> {
340                 RenderDataUtil.register(data, "path", file.getPath());
341             }).orElse(() -> {
342                 throwValidationError(messages -> messages.addErrorsFailedToDownloadStemmeroverrideFile(GLOBAL), this::asDictIndexHtml);
343             });
344         });
345     }
346 
347     /**
348      * Upload a new stemmer override dictionary file.
349      * Processes the uploaded file and updates the dictionary.
350      *
351      * @param form The upload form containing the file and dictionary ID
352      * @return HTML response redirecting to the list page on success
353      */
354     @Execute
355     @Secured({ ROLE })
356     public HtmlResponse upload(final UploadForm form) {
357         validate(form, messages -> {}, () -> uploadpage(form.dictId));
358         verifyToken(() -> uploadpage(form.dictId));
359         return stemmerOverrideService.getStemmerOverrideFile(form.dictId).map(file -> {
360             try (InputStream inputStream = form.stemmerOverrideFile.getInputStream()) {
361                 file.update(inputStream);
362             } catch (final IOException e) {
363                 logger.warn("Failed to process a request.", e);
364                 throwValidationError(messages -> messages.addErrorsFailedToUploadStemmeroverrideFile(GLOBAL),
365                         () -> redirectWith(getClass(), moreUrl("uploadpage/" + form.dictId)));
366             }
367             saveInfo(messages -> messages.addSuccessUploadStemmeroverrideFile(GLOBAL));
368             return redirectWith(getClass(), moreUrl("list/1").params("dictId", form.dictId));
369         }).orElseGet(() -> {
370             throwValidationError(messages -> messages.addErrorsFailedToUploadStemmeroverrideFile(GLOBAL), () -> uploadpage(form.dictId));
371             return null;
372         });
373 
374     }
375 
376     // -----------------------------------------------------
377     //                                         Actually Crud
378     //                                         -------------
379     /**
380      * Create a stemmer override item.
381      * @param form The create form.
382      * @return The HTML response.
383      */
384     @Execute
385     @Secured({ ROLE })
386     public HtmlResponse create(final CreateForm form) {
387         verifyCrudMode(form.crudMode, CrudMode.CREATE, form.dictId);
388         validate(form, messages -> {}, this::asEditHtml);
389         verifyToken(this::asEditHtml);
390         createStemmerOverrideItem(form, this::asEditHtml).ifPresent(entity -> {
391             try {
392                 stemmerOverrideService.store(form.dictId, entity);
393                 saveInfo(messages -> messages.addSuccessCrudCreateCrudTable(GLOBAL));
394             } catch (final Exception e) {
395                 logger.warn("Failed to process a request.", e);
396                 throwValidationError(messages -> messages.addErrorsCrudFailedToCreateCrudTable(GLOBAL, buildThrowableMessage(e)),
397                         this::asEditHtml);
398             }
399         }).orElse(() -> {
400             throwValidationError(messages -> messages.addErrorsCrudFailedToCreateInstance(GLOBAL), this::asEditHtml);
401         });
402         return redirectWith(getClass(), moreUrl("list/1").params("dictId", form.dictId));
403     }
404 
405     /**
406      * Update an existing stemmer override item.
407      * Validates the form data and updates the item in the dictionary.
408      *
409      * @param form The edit form containing updated item data
410      * @return HTML response redirecting to the list page on success
411      */
412     @Execute
413     @Secured({ ROLE })
414     public HtmlResponse update(final EditForm form) {
415         verifyCrudMode(form.crudMode, CrudMode.EDIT, form.dictId);
416         validate(form, messages -> {}, this::asEditHtml);
417         verifyToken(this::asEditHtml);
418         createStemmerOverrideItem(form, this::asEditHtml).ifPresent(entity -> {
419             try {
420                 stemmerOverrideService.store(form.dictId, entity);
421                 saveInfo(messages -> messages.addSuccessCrudUpdateCrudTable(GLOBAL));
422             } catch (final Exception e) {
423                 logger.warn("Failed to process a request.", e);
424                 throwValidationError(messages -> messages.addErrorsCrudFailedToUpdateCrudTable(GLOBAL, buildThrowableMessage(e)),
425                         this::asEditHtml);
426             }
427         }).orElse(() -> {
428             saveToken();
429             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, form.getDisplayId()), this::asEditHtml);
430         });
431         return redirectWith(getClass(), moreUrl("list/1").params("dictId", form.dictId));
432     }
433 
434     /**
435      * Delete a stemmer override item from the dictionary.
436      * Removes the specified item and redirects to the list page.
437      *
438      * @param form The edit form containing the item ID to delete
439      * @return HTML response redirecting to the list page on success
440      */
441     @Execute
442     @Secured({ ROLE })
443     public HtmlResponse delete(final EditForm form) {
444         verifyCrudMode(form.crudMode, CrudMode.DETAILS, form.dictId);
445         validate(form, messages -> {}, this::asDetailsHtml);
446         verifyToken(this::asDetailsHtml);
447         stemmerOverrideService.getStemmerOverrideItem(form.dictId, form.id).ifPresent(entity -> {
448             try {
449                 stemmerOverrideService.delete(form.dictId, entity);
450                 saveInfo(messages -> messages.addSuccessCrudDeleteCrudTable(GLOBAL));
451             } catch (final Exception e) {
452                 logger.warn("Failed to process a request.", e);
453                 throwValidationError(messages -> messages.addErrorsCrudFailedToDeleteCrudTable(GLOBAL, buildThrowableMessage(e)),
454                         this::asEditHtml);
455             }
456         }).orElse(() -> {
457             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, form.getDisplayId()), this::asDetailsHtml);
458         });
459         return redirectWith(getClass(), moreUrl("list/1").params("dictId", form.dictId));
460     }
461 
462     //===================================================================================
463     //                                                                        Assist Logic
464     //                                                                        ============
465 
466     private static OptionalEntity<StemmerOverrideItem> getEntity(final CreateForm form) {
467         switch (form.crudMode) {
468         case CrudMode.CREATE:
469             final StemmerOverrideItem entity = new StemmerOverrideItem(0, StringUtil.EMPTY, StringUtil.EMPTY);
470             return OptionalEntity.of(entity);
471         case CrudMode.EDIT:
472             if (form instanceof EditForm) {
473                 return ComponentUtil.getComponent(StemmerOverrideService.class).getStemmerOverrideItem(form.dictId, ((EditForm) form).id);
474             }
475             break;
476         default:
477             break;
478         }
479         return OptionalEntity.empty();
480     }
481 
482     /**
483      * Create a stemmer override item.
484      * @param form The create form.
485      * @param hook The error hook.
486      * @return An optional entity of a stemmer override item.
487      */
488     protected OptionalEntity<StemmerOverrideItem> createStemmerOverrideItem(final CreateForm form, final VaErrorHook hook) {
489         try {
490             return createStemmerOverrideItem(this, form, hook);
491         } catch (final ValidationErrorException e) {
492             saveToken();
493             throw e;
494         }
495     }
496 
497     /**
498      * Get the stemmer override item.
499      * @param action The action.
500      * @param form The create form.
501      * @param hook The error hook.
502      * @return The stemmer override item.
503      */
504     public static OptionalEntity<StemmerOverrideItem> createStemmerOverrideItem(final FessBaseAction action, final CreateForm form,
505             final VaErrorHook hook) {
506         return getEntity(form).map(entity -> {
507             entity.setNewInput(form.input);
508             entity.setNewOutput(form.output);
509             return entity;
510         });
511     }
512 
513     // ===================================================================================
514     //                                                                        Small Helper
515     //                                                                        ============
516     /**
517      * Verify that the CRUD mode matches the expected mode.
518      * Throws validation error if modes don't match.
519      *
520      * @param crudMode The current CRUD mode
521      * @param expectedMode The expected CRUD mode
522      * @param dictId The dictionary ID for error context
523      */
524     protected void verifyCrudMode(final int crudMode, final int expectedMode, final String dictId) {
525         if (crudMode != expectedMode) {
526             throwValidationError(messages -> {
527                 messages.addErrorsCrudInvalidMode(GLOBAL, String.valueOf(expectedMode), String.valueOf(crudMode));
528             }, () -> asListHtml(dictId));
529         }
530     }
531 
532     // ===================================================================================
533     //                                                                              JSP
534     //                                                                           =========
535 
536     /**
537      * Get the HTML response for the dictionary index page.
538      * @return The HTML response.
539      */
540     protected HtmlResponse asDictIndexHtml() {
541         return redirect(AdminDictAction.class);
542     }
543 
544     private HtmlResponse asListHtml(final String dictId) {
545         return asHtml(path_AdminDictStemmeroverride_AdminDictStemmeroverrideJsp).renderWith(data -> {
546             RenderDataUtil.register(data, "stemmerOverrideItemItems",
547                     stemmerOverrideService.getStemmerOverrideList(dictId, stemmerOverridePager));
548         }).useForm(SearchForm.class, setup -> {
549             setup.setup(form -> {
550                 copyBeanToBean(stemmerOverridePager, form, op -> op.include("id"));
551             });
552         });
553     }
554 
555     private HtmlResponse asEditHtml() {
556         return asHtml(path_AdminDictStemmeroverride_AdminDictStemmeroverrideEditJsp);
557     }
558 
559     private HtmlResponse asDetailsHtml() {
560         return asHtml(path_AdminDictStemmeroverride_AdminDictStemmeroverrideDetailsJsp);
561     }
562 
563 }