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.kuromoji;
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.KuromojiPager;
29  import org.codelibs.fess.app.service.KuromojiService;
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.kuromoji.KuromojiItem;
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 Kuromoji dictionary management.
51   *
52   */
53  public class AdminDictKuromojiAction extends FessAdminAction {
54  
55      /**
56       * Default constructor.
57       */
58      public AdminDictKuromojiAction() {
59          super();
60      }
61  
62      /** The role for this action. */
63      public static final String ROLE = "admin-dict";
64  
65      private static final Logger logger = LogManager.getLogger(AdminDictKuromojiAction.class);
66  
67      // ===================================================================================
68      //                                                                           Attribute
69      //                                                                           =========
70      @Resource
71      private KuromojiService kuromojiService;
72      @Resource
73      private KuromojiPager kuromojiPager;
74  
75      // ===================================================================================
76      //                                                                               Hook
77      //                                                                              ======
78      @Override
79      protected void setupHtmlData(final ActionRuntime runtime) {
80          super.setupHtmlData(runtime);
81          runtime.registerData("helpLink", systemHelper.getHelpLink(fessConfig.getOnlineHelpNameDictKuromoji()));
82      }
83  
84      @Override
85      protected String getActionRole() {
86          return ROLE;
87      }
88  
89      // ===================================================================================
90      //                                                                      Search Execute
91      //                                                                      ==============
92      /**
93       * Show the index page.
94       * @param form The search form.
95       * @return The HTML response.
96       */
97      @Execute
98      @Secured({ ROLE, ROLE + VIEW })
99      public HtmlResponse index(final SearchForm form) {
100         validate(form, messages -> {}, this::asDictIndexHtml);
101         kuromojiPager.clear();
102         return asHtml(path_AdminDictKuromoji_AdminDictKuromojiJsp).renderWith(data -> {
103             searchPaging(data, form);
104         });
105     }
106 
107     /**
108      * Show the list page.
109      * @param pageNumber The page number.
110      * @param form The search form.
111      * @return The HTML response.
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             kuromojiPager.setCurrentPageNumber(pageNumber.get());
119         }).orElse(() -> {
120             kuromojiPager.setCurrentPageNumber(0);
121         });
122         return asHtml(path_AdminDictKuromoji_AdminDictKuromojiJsp).renderWith(data -> {
123             searchPaging(data, form);
124         });
125     }
126 
127     /**
128      * Search for kuromoji items.
129      * @param form The search form.
130      * @return The HTML response.
131      */
132     @Execute
133     @Secured({ ROLE, ROLE + VIEW })
134     public HtmlResponse search(final SearchForm form) {
135         validate(form, messages -> {}, this::asDictIndexHtml);
136         copyBeanToBean(form, kuromojiPager, op -> op.exclude(Constants.PAGER_CONVERSION_RULE));
137         return asHtml(path_AdminDictKuromoji_AdminDictKuromojiJsp).renderWith(data -> {
138             searchPaging(data, form);
139         });
140     }
141 
142     /**
143      * Reset the search form.
144      * @param form The search form.
145      * @return The HTML response.
146      */
147     @Execute
148     @Secured({ ROLE, ROLE + VIEW })
149     public HtmlResponse reset(final SearchForm form) {
150         validate(form, messages -> {}, this::asDictIndexHtml);
151         kuromojiPager.clear();
152         return asHtml(path_AdminDictKuromoji_AdminDictKuromojiJsp).renderWith(data -> {
153             searchPaging(data, form);
154         });
155     }
156 
157     /**
158      * Search with paging.
159      * @param data The render data.
160      * @param form The search form.
161      */
162     protected void searchPaging(final RenderData data, final SearchForm form) {
163         // page navi
164         RenderDataUtil.register(data, "kuromojiItemItems", kuromojiService.getKuromojiList(form.dictId, kuromojiPager));
165 
166         // restore from pager
167         BeanUtil.copyBeanToBean(kuromojiPager, form, op -> {
168             op.exclude(Constants.PAGER_CONVERSION_RULE);
169         });
170     }
171 
172     // ===================================================================================
173     //                                                                        Edit Execute
174     //                                                                        ============
175     // -----------------------------------------------------
176     //                                            Entry Page
177     //                                            ----------
178     /**
179      * Show the create new page.
180      * @param dictId The dictionary ID.
181      * @return The HTML response.
182      */
183     @Execute
184     @Secured({ ROLE })
185     public HtmlResponse createnew(final String dictId) {
186         saveToken();
187         return asHtml(path_AdminDictKuromoji_AdminDictKuromojiEditJsp).useForm(CreateForm.class, op -> {
188             op.setup(form -> {
189                 form.initialize();
190                 form.crudMode = CrudMode.CREATE;
191                 form.dictId = dictId;
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 -> {}, () -> asListHtml(form.dictId));
205         kuromojiService.getKuromojiItem(form.dictId, form.id).ifPresent(entity -> {
206             copyBeanToBean(entity, form, op -> {});
207         }).orElse(() -> {
208             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, form.getDisplayId()),
209                     () -> asListHtml(form.dictId));
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 dictId The dictionary ID.
227      * @param crudMode The CRUD mode.
228      * @param id The ID of the kuromoji item.
229      * @return The HTML response.
230      */
231     @Execute
232     @Secured({ ROLE, ROLE + VIEW })
233     public HtmlResponse details(final String dictId, final int crudMode, final long id) {
234         verifyCrudMode(crudMode, CrudMode.DETAILS, dictId);
235         saveToken();
236         return asDetailsHtml().useForm(EditForm.class, op -> {
237             op.setup(form -> {
238                 kuromojiService.getKuromojiItem(dictId, id).ifPresent(entity -> {
239                     copyBeanToBean(entity, form, copyOp -> {
240                         copyOp.excludeNull();
241                     });
242                     form.crudMode = crudMode;
243                 }).orElse(() -> {
244                     throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, dictId + ":" + id),
245                             () -> asListHtml(dictId));
246                 });
247                 form.dictId = dictId;
248             });
249         });
250     }
251 
252     // -----------------------------------------------------
253     //                                              Download
254     //                                               -------
255     /**
256      * Show the download page.
257      * @param dictId The dictionary ID.
258      * @return The HTML response.
259      */
260     @Execute
261     @Secured({ ROLE, ROLE + VIEW })
262     public HtmlResponse downloadpage(final String dictId) {
263         saveToken();
264         return asHtml(path_AdminDictKuromoji_AdminDictKuromojiDownloadJsp).useForm(DownloadForm.class, op -> {
265             op.setup(form -> {
266                 form.dictId = dictId;
267             });
268         }).renderWith(data -> {
269             kuromojiService.getKuromojiFile(dictId).ifPresent(file -> {
270                 RenderDataUtil.register(data, "path", file.getPath());
271             }).orElse(() -> {
272                 throwValidationError(messages -> messages.addErrorsFailedToDownloadKuromojiFile(GLOBAL), this::asDictIndexHtml);
273             });
274         });
275     }
276 
277     /**
278      * Download a kuromoji dictionary file.
279      * @param form The download form.
280      * @return The action response.
281      */
282     @Execute
283     @Secured({ ROLE, ROLE + VIEW })
284     public ActionResponse download(final DownloadForm form) {
285         validate(form, messages -> {}, () -> downloadpage(form.dictId));
286         verifyTokenKeep(() -> downloadpage(form.dictId));
287         return kuromojiService.getKuromojiFile(form.dictId)
288                 .map(file -> asStream(new File(file.getPath()).getName()).contentTypeOctetStream().stream(out -> {
289                     file.writeOut(out);
290                 }))
291                 .orElseGet(() -> {
292                     throwValidationError(messages -> messages.addErrorsFailedToDownloadKuromojiFile(GLOBAL),
293                             () -> downloadpage(form.dictId));
294                     return null;
295                 });
296     }
297 
298     // -----------------------------------------------------
299     //                                                Upload
300     //                                               -------
301     /**
302      * Show the upload page.
303      * @param dictId The dictionary ID.
304      * @return The HTML response.
305      */
306     @Execute
307     @Secured({ ROLE })
308     public HtmlResponse uploadpage(final String dictId) {
309         saveToken();
310         return asHtml(path_AdminDictKuromoji_AdminDictKuromojiUploadJsp).useForm(UploadForm.class, op -> {
311             op.setup(form -> {
312                 form.dictId = dictId;
313             });
314         }).renderWith(data -> {
315             kuromojiService.getKuromojiFile(dictId).ifPresent(file -> {
316                 RenderDataUtil.register(data, "path", file.getPath());
317             }).orElse(() -> {
318                 throwValidationError(messages -> messages.addErrorsFailedToDownloadKuromojiFile(GLOBAL), this::asDictIndexHtml);
319             });
320         });
321     }
322 
323     /**
324      * Upload a kuromoji dictionary file.
325      * @param form The upload form.
326      * @return The HTML response.
327      */
328     @Execute
329     @Secured({ ROLE })
330     public HtmlResponse upload(final UploadForm form) {
331         validate(form, messages -> {}, () -> uploadpage(form.dictId));
332         verifyToken(() -> uploadpage(form.dictId));
333         return kuromojiService.getKuromojiFile(form.dictId).map(file -> {
334             try (InputStream inputStream = form.kuromojiFile.getInputStream()) {
335                 file.update(inputStream);
336             } catch (final IOException e) {
337                 logger.warn("Failed to process a request.", e);
338                 throwValidationError(messages -> messages.addErrorsFailedToUploadKuromojiFile(GLOBAL),
339                         () -> redirectWith(getClass(), moreUrl("uploadpage/" + form.dictId)));
340             }
341             saveInfo(messages -> messages.addSuccessUploadKuromojiFile(GLOBAL));
342             return redirectWith(getClass(), moreUrl("list/1").params("dictId", form.dictId));
343         }).orElseGet(() -> {
344             throwValidationError(messages -> messages.addErrorsFailedToUploadKuromojiFile(GLOBAL), () -> uploadpage(form.dictId));
345             return null;
346         });
347 
348     }
349 
350     // -----------------------------------------------------
351     //                                         Actually Crud
352     //                                         -------------
353     /**
354      * Create a new kuromoji item.
355      * @param form The create form.
356      * @return The HTML response.
357      */
358     @Execute
359     @Secured({ ROLE })
360     public HtmlResponse create(final CreateForm form) {
361         verifyCrudMode(form.crudMode, CrudMode.CREATE, form.dictId);
362         validate(form, messages -> {}, this::asEditHtml);
363         verifyForm(form);
364         verifyToken(this::asEditHtml);
365         createKuromojiItem(form, this::asEditHtml).ifPresent(entity -> {
366             try {
367                 kuromojiService.store(form.dictId, entity);
368                 saveInfo(messages -> messages.addSuccessCrudCreateCrudTable(GLOBAL));
369             } catch (final Exception e) {
370                 logger.warn("Failed to process a request.", e);
371                 throwValidationError(messages -> messages.addErrorsCrudFailedToCreateCrudTable(GLOBAL, buildThrowableMessage(e)),
372                         this::asEditHtml);
373             }
374         }).orElse(() -> {
375             throwValidationError(messages -> messages.addErrorsCrudFailedToCreateInstance(GLOBAL), this::asEditHtml);
376         });
377         return redirectWith(getClass(), moreUrl("list/1").params("dictId", form.dictId));
378     }
379 
380     /**
381      * Update a kuromoji item.
382      * @param form The edit form.
383      * @return The HTML response.
384      */
385     @Execute
386     @Secured({ ROLE })
387     public HtmlResponse update(final EditForm form) {
388         verifyCrudMode(form.crudMode, CrudMode.EDIT, form.dictId);
389         validate(form, messages -> {}, this::asEditHtml);
390         verifyForm(form);
391         verifyToken(this::asEditHtml);
392         createKuromojiItem(form, this::asEditHtml).ifPresent(entity -> {
393             try {
394                 kuromojiService.store(form.dictId, entity);
395                 saveInfo(messages -> messages.addSuccessCrudUpdateCrudTable(GLOBAL));
396             } catch (final Exception e) {
397                 logger.warn("Failed to process a request.", e);
398                 throwValidationError(messages -> messages.addErrorsCrudFailedToUpdateCrudTable(GLOBAL, buildThrowableMessage(e)),
399                         this::asEditHtml);
400             }
401         }).orElse(() -> {
402             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, form.getDisplayId()), this::asEditHtml);
403         });
404         return redirectWith(getClass(), moreUrl("list/1").params("dictId", form.dictId));
405     }
406 
407     /**
408      * Delete a kuromoji item.
409      * @param form The edit form.
410      * @return The HTML response.
411      */
412     @Execute
413     @Secured({ ROLE })
414     public HtmlResponse delete(final EditForm form) {
415         verifyCrudMode(form.crudMode, CrudMode.DETAILS, form.dictId);
416         validate(form, messages -> {}, this::asDetailsHtml);
417         verifyToken(this::asDetailsHtml);
418         kuromojiService.getKuromojiItem(form.dictId, form.id).ifPresent(entity -> {
419             try {
420                 kuromojiService.delete(form.dictId, entity);
421                 saveInfo(messages -> messages.addSuccessCrudDeleteCrudTable(GLOBAL));
422             } catch (final Exception e) {
423                 logger.warn("Failed to process a request.", e);
424                 throwValidationError(messages -> messages.addErrorsCrudFailedToDeleteCrudTable(GLOBAL, buildThrowableMessage(e)),
425                         this::asEditHtml);
426             }
427         }).orElse(() -> {
428             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, form.getDisplayId()), this::asDetailsHtml);
429         });
430         return redirectWith(getClass(), moreUrl("list/1").params("dictId", form.dictId));
431     }
432 
433     //===================================================================================
434     //                                                                        Assist Logic
435     //                                                                        ============
436 
437     private static OptionalEntity<KuromojiItem> getEntity(final CreateForm form) {
438         switch (form.crudMode) {
439         case CrudMode.CREATE:
440             final KuromojiItem entity = new KuromojiItem(0, StringUtil.EMPTY, StringUtil.EMPTY, StringUtil.EMPTY, StringUtil.EMPTY);
441             return OptionalEntity.of(entity);
442         case CrudMode.EDIT:
443             if (form instanceof EditForm) {
444                 return ComponentUtil.getComponent(KuromojiService.class).getKuromojiItem(form.dictId, ((EditForm) form).id);
445             }
446             break;
447         default:
448             break;
449         }
450         return OptionalEntity.empty();
451     }
452 
453     /**
454      * Create a kuromoji item.
455      * @param form The create form.
456      * @param hook The error hook.
457      * @return An optional entity of a kuromoji item.
458      */
459     protected OptionalEntity<KuromojiItem> createKuromojiItem(final CreateForm form, final VaErrorHook hook) {
460         try {
461             return createKuromojiItem(this, form, hook);
462         } catch (final ValidationErrorException e) {
463             saveToken();
464             throw e;
465         }
466     }
467 
468     /**
469      * Create a kuromoji item.
470      * @param action The action.
471      * @param form The create form.
472      * @param hook The error hook.
473      * @return An optional entity of a kuromoji item.
474      */
475     public static OptionalEntity<KuromojiItem> createKuromojiItem(final FessBaseAction action, final CreateForm form,
476             final VaErrorHook hook) {
477         return getEntity(form).map(entity -> {
478             entity.setNewToken(form.token);
479             entity.setNewSegmentation(form.segmentation);
480             entity.setNewReading(form.reading);
481             entity.setNewPos(form.pos);
482             return entity;
483         });
484     }
485 
486     // ===================================================================================
487     //                                                                        Small Helper
488     //                                                                        ============
489     /**
490      * Verify the CRUD mode.
491      * @param crudMode The CRUD mode.
492      * @param expectedMode The expected mode.
493      * @param dictId The dictionary ID.
494      */
495     protected void verifyCrudMode(final int crudMode, final int expectedMode, final String dictId) {
496         if (crudMode != expectedMode) {
497             throwValidationError(messages -> {
498                 messages.addErrorsCrudInvalidMode(GLOBAL, String.valueOf(expectedMode), String.valueOf(crudMode));
499             }, () -> asListHtml(dictId));
500         }
501     }
502 
503     /**
504      * Verify the form.
505      * @param form The create form.
506      */
507     protected void verifyForm(final CreateForm form) {
508         if (form.token != null && form.token.split(" ").length > 1) {
509             throwValidationError(messages -> {
510                 messages.addErrorsInvalidKuromojiToken("token", form.token);
511             }, this::asEditHtml);
512         }
513         if (form.segmentation != null && form.reading != null && form.segmentation.split(" ").length != form.reading.split(" ").length) {
514             throwValidationError(messages -> {
515                 messages.addErrorsInvalidKuromojiSegmentation("segmentation", form.segmentation, form.reading);
516             }, this::asEditHtml);
517         }
518     }
519 
520     // ===================================================================================
521     //                                                                              JSP
522     //                                                                           =========
523 
524     /**
525      * Get the HTML response for the dictionary index page.
526      * @return The HTML response.
527      */
528     protected HtmlResponse asDictIndexHtml() {
529         return redirect(AdminDictAction.class);
530     }
531 
532     private HtmlResponse asListHtml(final String dictId) {
533         return asHtml(path_AdminDictKuromoji_AdminDictKuromojiJsp).renderWith(data -> {
534             RenderDataUtil.register(data, "kuromojiItemItems", kuromojiService.getKuromojiList(dictId, kuromojiPager));
535         }).useForm(SearchForm.class, setup -> {
536             setup.setup(form -> {
537                 copyBeanToBean(kuromojiPager, form, op -> op.include("id"));
538             });
539         });
540     }
541 
542     private HtmlResponse asEditHtml() {
543         return asHtml(path_AdminDictKuromoji_AdminDictKuromojiEditJsp);
544     }
545 
546     private HtmlResponse asDetailsHtml() {
547         return asHtml(path_AdminDictKuromoji_AdminDictKuromojiDetailsJsp);
548     }
549 
550 }