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.synonym;
17  
18  import java.io.File;
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.util.ArrayList;
22  import java.util.List;
23  
24  import org.apache.logging.log4j.LogManager;
25  import org.apache.logging.log4j.Logger;
26  import org.codelibs.core.beans.util.BeanUtil;
27  import org.codelibs.core.lang.StringUtil;
28  import org.codelibs.fess.Constants;
29  import org.codelibs.fess.annotation.Secured;
30  import org.codelibs.fess.app.pager.SynonymPager;
31  import org.codelibs.fess.app.service.SynonymService;
32  import org.codelibs.fess.app.web.CrudMode;
33  import org.codelibs.fess.app.web.admin.dict.AdminDictAction;
34  import org.codelibs.fess.app.web.base.FessAdminAction;
35  import org.codelibs.fess.app.web.base.FessBaseAction;
36  import org.codelibs.fess.dict.synonym.SynonymItem;
37  import org.codelibs.fess.util.ComponentUtil;
38  import org.codelibs.fess.util.RenderDataUtil;
39  import org.dbflute.optional.OptionalEntity;
40  import org.dbflute.optional.OptionalThing;
41  import org.lastaflute.web.Execute;
42  import org.lastaflute.web.response.ActionResponse;
43  import org.lastaflute.web.response.HtmlResponse;
44  import org.lastaflute.web.response.render.RenderData;
45  import org.lastaflute.web.ruts.process.ActionRuntime;
46  import org.lastaflute.web.validation.VaErrorHook;
47  import org.lastaflute.web.validation.exception.ValidationErrorException;
48  
49  import jakarta.annotation.Resource;
50  
51  /**
52   * Admin action for Synonym management.
53   *
54   */
55  public class AdminDictSynonymAction extends FessAdminAction {
56  
57      /**
58       * Role name required for accessing synonym dictionary administration features.
59       */
60      public static final String ROLE = "admin-dict";
61  
62      private static final Logger logger = LogManager.getLogger(AdminDictSynonymAction.class);
63  
64      // ===================================================================================
65      //                                                                           Attribute
66      //                                                                           =========
67      @Resource
68      private SynonymService synonymService;
69      @Resource
70      private SynonymPager synonymPager;
71  
72      /**
73       * Default constructor.
74       */
75      public AdminDictSynonymAction() {
76          super();
77      }
78  
79      // ===================================================================================
80      //                                                                               Hook
81      //                                                                              ======
82      @Override
83      protected void setupHtmlData(final ActionRuntime runtime) {
84          super.setupHtmlData(runtime);
85          runtime.registerData("helpLink", systemHelper.getHelpLink(fessConfig.getOnlineHelpNameDictSynonym()));
86      }
87  
88      @Override
89      protected String getActionRole() {
90          return ROLE;
91      }
92  
93      // ===================================================================================
94      //                                                                      Search Execute
95      //                                                                      ==============
96      /**
97       * Displays the main synonym dictionary index page.
98       *
99       * @param form the search form containing search criteria
100      * @return HTML response for the synonym dictionary index page
101      */
102     @Execute
103     @Secured({ ROLE, ROLE + VIEW })
104     public HtmlResponse index(final SearchForm form) {
105         validate(form, messages -> {}, this::asDictIndexHtml);
106         synonymPager.clear();
107         return asHtml(path_AdminDictSynonym_AdminDictSynonymJsp).renderWith(data -> {
108             searchPaging(data, form);
109         });
110     }
111 
112     /**
113      * Displays a paginated list of synonym items.
114      *
115      * @param pageNumber the optional page number for pagination
116      * @param form the search form containing search criteria
117      * @return HTML response with the synonym items 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             synonymPager.setCurrentPageNumber(pageNumber.get());
125         }).orElse(() -> {
126             synonymPager.setCurrentPageNumber(0);
127         });
128         return asHtml(path_AdminDictSynonym_AdminDictSynonymJsp).renderWith(data -> {
129             searchPaging(data, form);
130         });
131     }
132 
133     /**
134      * Performs a search for synonym items based on the provided criteria.
135      *
136      * @param form the search form containing search criteria
137      * @return HTML response with the search results
138      */
139     @Execute
140     @Secured({ ROLE, ROLE + VIEW })
141     public HtmlResponse search(final SearchForm form) {
142         validate(form, messages -> {}, this::asDictIndexHtml);
143         copyBeanToBean(form, synonymPager, op -> op.exclude(Constants.PAGER_CONVERSION_RULE));
144         return asHtml(path_AdminDictSynonym_AdminDictSynonymJsp).renderWith(data -> {
145             searchPaging(data, form);
146         });
147     }
148 
149     /**
150      * Resets the search criteria and returns to the default view.
151      *
152      * @param form the search form to reset
153      * @return HTML response with reset search criteria
154      */
155     @Execute
156     @Secured({ ROLE, ROLE + VIEW })
157     public HtmlResponse reset(final SearchForm form) {
158         validate(form, messages -> {}, this::asDictIndexHtml);
159         synonymPager.clear();
160         return asHtml(path_AdminDictSynonym_AdminDictSynonymJsp).renderWith(data -> {
161             searchPaging(data, form);
162         });
163     }
164 
165     /**
166      * Sets up pagination data for search results.
167      *
168      * @param data the render data to populate
169      * @param form the search form containing criteria
170      */
171     protected void searchPaging(final RenderData data, final SearchForm form) {
172         // page navi
173         RenderDataUtil.register(data, "synonymItemItems", synonymService.getSynonymList(form.dictId, synonymPager));
174 
175         // restore from pager
176         BeanUtil.copyBeanToBean(synonymPager, form, op -> {
177             op.exclude(Constants.PAGER_CONVERSION_RULE);
178         });
179     }
180 
181     // ===================================================================================
182     //                                                                        Edit Execute
183     //                                                                        ============
184     // -----------------------------------------------------
185     //                                            Entry Page
186     //                                            ----------
187     /**
188      * Displays the form for creating a new synonym item.
189      *
190      * @param dictId the dictionary ID
191      * @return HTML response for the create new synonym form
192      */
193     @Execute
194     @Secured({ ROLE })
195     public HtmlResponse createnew(final String dictId) {
196         saveToken();
197         return asHtml(path_AdminDictSynonym_AdminDictSynonymEditJsp).useForm(CreateForm.class, op -> {
198             op.setup(form -> {
199                 form.initialize();
200                 form.crudMode = CrudMode.CREATE;
201                 form.dictId = dictId;
202             });
203         });
204     }
205 
206     /**
207      * Displays the form for editing an existing synonym item.
208      *
209      * @param form the edit form containing synonym item data
210      * @return HTML response for the edit synonym form
211      */
212     @Execute
213     @Secured({ ROLE })
214     public HtmlResponse edit(final EditForm form) {
215         validate(form, messages -> {}, () -> asListHtml(form.dictId));
216         synonymService.getSynonymItem(form.dictId, form.id).ifPresent(entity -> {
217             form.inputs = entity.getInputsValue();
218             form.outputs = entity.getOutputsValue();
219         }).orElse(() -> {
220             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, form.getDisplayId()),
221                     () -> asListHtml(form.dictId));
222         });
223         saveToken();
224         if (form.crudMode.intValue() == CrudMode.EDIT) {
225             // back
226             form.crudMode = CrudMode.DETAILS;
227             return asDetailsHtml();
228         }
229         form.crudMode = CrudMode.EDIT;
230         return asEditHtml();
231     }
232 
233     // -----------------------------------------------------
234     //                                               Details
235     //                                               -------
236     /**
237      * Displays the details view for a specific synonym item.
238      *
239      * @param dictId the dictionary ID
240      * @param crudMode the CRUD operation mode
241      * @param id the synonym item ID
242      * @return HTML response for the synonym item details
243      */
244     @Execute
245     @Secured({ ROLE, ROLE + VIEW })
246     public HtmlResponse details(final String dictId, final int crudMode, final long id) {
247         verifyCrudMode(crudMode, CrudMode.DETAILS, dictId);
248         saveToken();
249         return asDetailsHtml().useForm(EditForm.class, op -> {
250             op.setup(form -> {
251                 synonymService.getSynonymItem(dictId, id).ifPresent(entity -> {
252                     form.inputs = entity.getInputsValue();
253                     form.outputs = entity.getOutputsValue();
254                 }).orElse(() -> {
255                     throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, dictId + ":" + id),
256                             () -> asListHtml(dictId));
257                 });
258                 form.id = id;
259                 form.crudMode = crudMode;
260                 form.dictId = dictId;
261             });
262         });
263     }
264 
265     // -----------------------------------------------------
266     //                                              Download
267     //                                               -------
268     /**
269      * Displays the download page for synonym dictionary files.
270      *
271      * @param dictId the dictionary ID
272      * @return HTML response for the download page
273      */
274     @Execute
275     @Secured({ ROLE, ROLE + VIEW })
276     public HtmlResponse downloadpage(final String dictId) {
277         saveToken();
278         return asHtml(path_AdminDictSynonym_AdminDictSynonymDownloadJsp).useForm(DownloadForm.class, op -> {
279             op.setup(form -> {
280                 form.dictId = dictId;
281             });
282         }).renderWith(data -> {
283             synonymService.getSynonymFile(dictId).ifPresent(file -> {
284                 RenderDataUtil.register(data, "path", file.getPath());
285             }).orElse(() -> {
286                 throwValidationError(messages -> messages.addErrorsFailedToDownloadSynonymFile(GLOBAL), this::asDictIndexHtml);
287             });
288         });
289     }
290 
291     /**
292      * Downloads the synonym dictionary file.
293      *
294      * @param form the download form containing download parameters
295      * @return ActionResponse with the file download stream
296      */
297     @Execute
298     @Secured({ ROLE, ROLE + VIEW })
299     public ActionResponse download(final DownloadForm form) {
300         validate(form, messages -> {}, () -> downloadpage(form.dictId));
301         verifyTokenKeep(() -> downloadpage(form.dictId));
302         return synonymService.getSynonymFile(form.dictId)
303                 .map(file -> asStream(new File(file.getPath()).getName()).contentTypeOctetStream().stream(out -> {
304                     file.writeOut(out);
305                 }))
306                 .orElseGet(() -> {
307                     throwValidationError(messages -> messages.addErrorsFailedToDownloadSynonymFile(GLOBAL),
308                             () -> downloadpage(form.dictId));
309                     return null;
310                 });
311     }
312 
313     // -----------------------------------------------------
314     //                                                Upload
315     //                                               -------
316     /**
317      * Displays the upload page for synonym dictionary files.
318      *
319      * @param dictId the dictionary ID
320      * @return HTML response for the upload page
321      */
322     @Execute
323     @Secured({ ROLE })
324     public HtmlResponse uploadpage(final String dictId) {
325         saveToken();
326         return asHtml(path_AdminDictSynonym_AdminDictSynonymUploadJsp).useForm(UploadForm.class, op -> {
327             op.setup(form -> {
328                 form.dictId = dictId;
329             });
330         }).renderWith(data -> {
331             synonymService.getSynonymFile(dictId).ifPresent(file -> {
332                 RenderDataUtil.register(data, "path", file.getPath());
333             }).orElse(() -> {
334                 throwValidationError(messages -> messages.addErrorsFailedToDownloadSynonymFile(GLOBAL), this::asDictIndexHtml);
335             });
336         });
337     }
338 
339     /**
340      * Handles the upload of synonym dictionary files.
341      *
342      * @param form the upload form containing the file to upload
343      * @return HTML response after processing the upload
344      */
345     @Execute
346     @Secured({ ROLE })
347     public HtmlResponse upload(final UploadForm form) {
348         validate(form, messages -> {}, () -> uploadpage(form.dictId));
349         verifyToken(() -> uploadpage(form.dictId));
350         return synonymService.getSynonymFile(form.dictId).map(file -> {
351             try (InputStream inputStream = form.synonymFile.getInputStream()) {
352                 file.update(inputStream);
353             } catch (final IOException e) {
354                 logger.warn("Failed to process a request.", e);
355                 throwValidationError(messages -> messages.addErrorsFailedToUploadSynonymFile(GLOBAL),
356                         () -> redirectWith(getClass(), moreUrl("uploadpage/" + form.dictId)));
357             }
358             saveInfo(messages -> messages.addSuccessUploadSynonymFile(GLOBAL));
359             return redirectWith(getClass(), moreUrl("list/1").params("dictId", form.dictId));
360         }).orElseGet(() -> {
361             throwValidationError(messages -> messages.addErrorsFailedToUploadSynonymFile(GLOBAL), () -> uploadpage(form.dictId));
362             return null;
363         });
364 
365     }
366 
367     // -----------------------------------------------------
368     //                                         Actually Crud
369     //                                         -------------
370     /**
371      * Creates a new synonym item.
372      *
373      * @param form the create form containing synonym item data
374      * @return HTML response after creating the synonym item
375      */
376     @Execute
377     @Secured({ ROLE })
378     public HtmlResponse create(final CreateForm form) {
379         verifyCrudMode(form.crudMode, CrudMode.CREATE, form.dictId);
380         validate(form, messages -> {}, this::asEditHtml);
381         verifyToken(this::asEditHtml);
382         createSynonymItem(form, this::asEditHtml).ifPresent(entity -> {
383             try {
384                 synonymService.store(form.dictId, entity);
385                 saveInfo(messages -> messages.addSuccessCrudCreateCrudTable(GLOBAL));
386             } catch (final Exception e) {
387                 logger.warn("Failed to process a request.", e);
388                 throwValidationError(messages -> messages.addErrorsCrudFailedToCreateCrudTable(GLOBAL, buildThrowableMessage(e)),
389                         this::asEditHtml);
390             }
391         }).orElse(() -> {
392             throwValidationError(messages -> messages.addErrorsCrudFailedToCreateInstance(GLOBAL), this::asEditHtml);
393         });
394         return redirectWith(getClass(), moreUrl("list/1").params("dictId", form.dictId));
395     }
396 
397     /**
398      * Updates an existing synonym item.
399      *
400      * @param form the edit form containing updated synonym item data
401      * @return HTML response after updating the synonym item
402      */
403     @Execute
404     @Secured({ ROLE })
405     public HtmlResponse update(final EditForm form) {
406         verifyCrudMode(form.crudMode, CrudMode.EDIT, form.dictId);
407         validate(form, messages -> {}, this::asEditHtml);
408         verifyToken(this::asEditHtml);
409         createSynonymItem(form, this::asEditHtml).ifPresent(entity -> {
410             try {
411                 synonymService.store(form.dictId, entity);
412                 saveInfo(messages -> messages.addSuccessCrudUpdateCrudTable(GLOBAL));
413             } catch (final Exception e) {
414                 logger.warn("Failed to process a request.", e);
415                 throwValidationError(messages -> messages.addErrorsCrudFailedToUpdateCrudTable(GLOBAL, buildThrowableMessage(e)),
416                         this::asEditHtml);
417             }
418         }).orElse(() -> {
419             saveToken();
420             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, form.getDisplayId()), this::asEditHtml);
421         });
422         return redirectWith(getClass(), moreUrl("list/1").params("dictId", form.dictId));
423     }
424 
425     /**
426      * Deletes an existing synonym item.
427      *
428      * @param form the edit form containing the synonym item to delete
429      * @return HTML response after deleting the synonym item
430      */
431     @Execute
432     @Secured({ ROLE })
433     public HtmlResponse delete(final EditForm form) {
434         verifyCrudMode(form.crudMode, CrudMode.DETAILS, form.dictId);
435         validate(form, messages -> {}, this::asDetailsHtml);
436         verifyToken(this::asDetailsHtml);
437         synonymService.getSynonymItem(form.dictId, form.id).ifPresent(entity -> {
438             try {
439                 synonymService.delete(form.dictId, entity);
440                 saveInfo(messages -> messages.addSuccessCrudDeleteCrudTable(GLOBAL));
441             } catch (final Exception e) {
442                 logger.warn("Failed to process a request.", e);
443                 throwValidationError(messages -> messages.addErrorsCrudFailedToDeleteCrudTable(GLOBAL, buildThrowableMessage(e)),
444                         this::asEditHtml);
445             }
446         }).orElse(() -> {
447             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, form.getDisplayId()), this::asDetailsHtml);
448         });
449         return redirectWith(getClass(), moreUrl("list/1").params("dictId", form.dictId));
450     }
451 
452     //===================================================================================
453     //                                                                        Assist Logic
454     //                                                                        ============
455 
456     private static OptionalEntity<SynonymItem> getEntity(final CreateForm form) {
457         switch (form.crudMode) {
458         case CrudMode.CREATE:
459             final SynonymItem entity = new SynonymItem(0, StringUtil.EMPTY_STRINGS, StringUtil.EMPTY_STRINGS);
460             return OptionalEntity.of(entity);
461         case CrudMode.EDIT:
462             if (form instanceof EditForm) {
463                 return ComponentUtil.getComponent(SynonymService.class).getSynonymItem(form.dictId, ((EditForm) form).id);
464             }
465             break;
466         default:
467             break;
468         }
469         return OptionalEntity.empty();
470     }
471 
472     /**
473      * Creates a synonym item from the provided form data with validation.
474      *
475      * @param form the create form containing synonym data
476      * @param hook the validation error hook for handling errors
477      * @return OptionalEntity containing the created synonym item or empty if creation failed
478      */
479     protected OptionalEntity<SynonymItem> createSynonymItem(final CreateForm form, final VaErrorHook hook) {
480         try {
481             return createSynonymItem(this, form, hook);
482         } catch (final ValidationErrorException e) {
483             saveToken();
484             throw e;
485         }
486     }
487 
488     /**
489      * Static method to create a synonym item from form data with validation.
490      *
491      * @param action the base action for validation operations
492      * @param form the create form containing synonym data
493      * @param hook the validation error hook for handling errors
494      * @return OptionalEntity containing the created synonym item or empty if creation failed
495      */
496     public static OptionalEntity<SynonymItem> createSynonymItem(final FessBaseAction action, final CreateForm form,
497             final VaErrorHook hook) {
498         return getEntity(form).map(entity -> {
499             final String[] newInputs = splitLine(form.inputs);
500             validateSynonymString(action, newInputs, "inputs", hook);
501             entity.setNewInputs(newInputs);
502             final String[] newOutputs = splitLine(form.outputs);
503             validateSynonymString(action, newOutputs, "outputs", hook);
504             entity.setNewOutputs(newOutputs);
505             return entity;
506         });
507     }
508 
509     // ===================================================================================
510     //                                                                        Small Helper
511     //                                                                        ============
512     /**
513      * Verifies that the CRUD mode matches the expected mode.
514      *
515      * @param crudMode the current CRUD mode
516      * @param expectedMode the expected CRUD mode
517      * @param dictId the dictionary ID for error handling
518      */
519     protected void verifyCrudMode(final int crudMode, final int expectedMode, final String dictId) {
520         if (crudMode != expectedMode) {
521             throwValidationError(messages -> {
522                 messages.addErrorsCrudInvalidMode(GLOBAL, String.valueOf(expectedMode), String.valueOf(crudMode));
523             }, () -> asListHtml(dictId));
524         }
525     }
526 
527     private static void validateSynonymString(final FessBaseAction action, final String[] values, final String propertyName,
528             final VaErrorHook hook) {
529         if (values.length == 0) {
530             return;
531         }
532         for (final String value : values) {
533             if (value.indexOf(',') >= 0) {
534                 action.throwValidationError(messages -> {
535                     messages.addErrorsInvalidStrIsIncluded(propertyName, value, ",");
536                 }, hook);
537             }
538             if (value.indexOf("=>") >= 0) {
539                 action.throwValidationError(messages -> {
540                     messages.addErrorsInvalidStrIsIncluded(propertyName, value, "=>");
541                 }, hook);
542             }
543         }
544     }
545 
546     private static String[] splitLine(final String value) {
547         if (StringUtil.isBlank(value)) {
548             return StringUtil.EMPTY_STRINGS;
549         }
550         final String[] values = value.split("[\r\n]");
551         final List<String> list = new ArrayList<>(values.length);
552         for (final String line : values) {
553             if (StringUtil.isNotBlank(line)) {
554                 list.add(line.trim());
555             }
556         }
557         return list.toArray(new String[list.size()]);
558     }
559 
560     // ===================================================================================
561     //                                                                              JSP
562     //                                                                           =========
563 
564     /**
565      * Redirects to the dictionary index page.
566      *
567      * @return HTML response redirecting to the dictionary index
568      */
569     protected HtmlResponse asDictIndexHtml() {
570         return redirect(AdminDictAction.class);
571     }
572 
573     private HtmlResponse asListHtml(final String dictId) {
574         return asHtml(path_AdminDictSynonym_AdminDictSynonymJsp).renderWith(data -> {
575             RenderDataUtil.register(data, "synonymItemItems", synonymService.getSynonymList(dictId, synonymPager));
576         }).useForm(SearchForm.class, setup -> {
577             setup.setup(form -> {
578                 copyBeanToBean(synonymPager, form, op -> op.include("id"));
579             });
580         });
581     }
582 
583     private HtmlResponse asEditHtml() {
584         return asHtml(path_AdminDictSynonym_AdminDictSynonymEditJsp);
585     }
586 
587     private HtmlResponse asDetailsHtml() {
588         return asHtml(path_AdminDictSynonym_AdminDictSynonymDetailsJsp);
589     }
590 
591 }