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