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.elevateword;
17  
18  import static org.codelibs.core.stream.StreamUtil.split;
19  import static org.codelibs.core.stream.StreamUtil.stream;
20  
21  import java.io.BufferedReader;
22  import java.io.BufferedWriter;
23  import java.io.InputStream;
24  import java.io.InputStreamReader;
25  import java.io.OutputStreamWriter;
26  import java.io.Reader;
27  import java.io.Writer;
28  import java.nio.file.Files;
29  import java.nio.file.Path;
30  import java.util.stream.Collectors;
31  import java.util.stream.Stream;
32  
33  import org.apache.logging.log4j.LogManager;
34  import org.apache.logging.log4j.Logger;
35  import org.codelibs.core.beans.util.BeanUtil;
36  import org.codelibs.core.concurrent.CommonPoolUtil;
37  import org.codelibs.core.lang.StringUtil;
38  import org.codelibs.fess.Constants;
39  import org.codelibs.fess.annotation.Secured;
40  import org.codelibs.fess.app.pager.ElevateWordPager;
41  import org.codelibs.fess.app.service.ElevateWordService;
42  import org.codelibs.fess.app.service.LabelTypeService;
43  import org.codelibs.fess.app.web.CrudMode;
44  import org.codelibs.fess.app.web.base.FessAdminAction;
45  import org.codelibs.fess.exception.FessSystemException;
46  import org.codelibs.fess.helper.PermissionHelper;
47  import org.codelibs.fess.helper.SuggestHelper;
48  import org.codelibs.fess.helper.SystemHelper;
49  import org.codelibs.fess.opensearch.config.exentity.ElevateWord;
50  import org.codelibs.fess.util.ComponentUtil;
51  import org.codelibs.fess.util.RenderDataUtil;
52  import org.dbflute.optional.OptionalEntity;
53  import org.dbflute.optional.OptionalThing;
54  import org.lastaflute.web.Execute;
55  import org.lastaflute.web.response.ActionResponse;
56  import org.lastaflute.web.response.HtmlResponse;
57  import org.lastaflute.web.response.render.RenderData;
58  import org.lastaflute.web.ruts.process.ActionRuntime;
59  
60  import jakarta.annotation.Resource;
61  
62  /**
63   * Admin action for Elevate Word management.
64   *
65   */
66  public class AdminElevatewordAction extends FessAdminAction {
67  
68      /**
69       * Default constructor.
70       */
71      public AdminElevatewordAction() {
72          super();
73      }
74  
75      /** Role constant for admin elevate word management access control. */
76      public static final String ROLE = "admin-elevateword";
77  
78      private static final Logger logger = LogManager.getLogger(AdminElevatewordAction.class);
79  
80      // ===================================================================================
81      //                                                                           Attribute
82      //                                                                           =========
83      @Resource
84      private ElevateWordService elevateWordService;
85      @Resource
86      private ElevateWordPager elevateWordPager;
87      /** Helper for managing search suggestions and elevate words. */
88      @Resource
89      protected SuggestHelper suggestHelper;
90      @Resource
91      private LabelTypeService labelTypeService;
92  
93      // ===================================================================================
94      //                                                                               Hook
95      //                                                                              ======
96      @Override
97      protected void setupHtmlData(final ActionRuntime runtime) {
98          super.setupHtmlData(runtime);
99          runtime.registerData("helpLink", systemHelper.getHelpLink(fessConfig.getOnlineHelpNameElevateword()));
100     }
101 
102     @Override
103     protected String getActionRole() {
104         return ROLE;
105     }
106 
107     // ===================================================================================
108     //                                                                      Search Execute
109     //                                                                      ==============
110     /**
111      * Shows the main elevate word management page.
112      *
113      * @return HTML response for the elevate word list page
114      */
115     @Execute
116     @Secured({ ROLE, ROLE + VIEW })
117     public HtmlResponse index() {
118         return asListHtml();
119     }
120 
121     /**
122      * Lists elevate words with pagination support.
123      *
124      * @param pageNumber optional page number for pagination
125      * @param form search form containing filter criteria
126      * @return HTML response with elevate word list
127      */
128     @Execute
129     @Secured({ ROLE, ROLE + VIEW })
130     public HtmlResponse list(final OptionalThing<Integer> pageNumber, final SearchForm form) {
131         pageNumber.ifPresent(num -> {
132             elevateWordPager.setCurrentPageNumber(pageNumber.get());
133         }).orElse(() -> {
134             elevateWordPager.setCurrentPageNumber(0);
135         });
136         return asHtml(path_AdminElevateword_AdminElevatewordJsp).renderWith(data -> {
137             searchPaging(data, form);
138         });
139     }
140 
141     /**
142      * Searches elevate words based on form criteria.
143      *
144      * @param form search form containing search parameters
145      * @return HTML response with filtered elevate word list
146      */
147     @Execute
148     @Secured({ ROLE, ROLE + VIEW })
149     public HtmlResponse search(final SearchForm form) {
150         copyBeanToBean(form, elevateWordPager, op -> op.exclude(Constants.PAGER_CONVERSION_RULE));
151         return asHtml(path_AdminElevateword_AdminElevatewordJsp).renderWith(data -> {
152             searchPaging(data, form);
153         });
154     }
155 
156     /**
157      * Resets the search criteria and shows all elevate words.
158      *
159      * @param form search form to be reset
160      * @return HTML response with all elevate words
161      */
162     @Execute
163     @Secured({ ROLE, ROLE + VIEW })
164     public HtmlResponse reset(final SearchForm form) {
165         elevateWordPager.clear();
166         return asHtml(path_AdminElevateword_AdminElevatewordJsp).renderWith(data -> {
167             searchPaging(data, form);
168         });
169     }
170 
171     /**
172      * Handles search pagination and data preparation for rendering.
173      *
174      * @param data render data to populate with search results
175      * @param form search form containing current search state
176      */
177     protected void searchPaging(final RenderData data, final SearchForm form) {
178         RenderDataUtil.register(data, "elevateWordItems", elevateWordService.getElevateWordList(elevateWordPager)); // page navi
179 
180         // restore from pager
181         copyBeanToBean(elevateWordPager, form, op -> op.include("id"));
182     }
183 
184     // ===================================================================================
185     //                                                                        Edit Execute
186     //                                                                        ============
187     // -----------------------------------------------------
188     //                                            Entry Page
189     //                                            ----------
190     /**
191      * Shows the form for creating a new elevate word.
192      *
193      * @return HTML response for the create elevate word form
194      */
195     @Execute
196     @Secured({ ROLE })
197     public HtmlResponse createnew() {
198         saveToken();
199         return asHtml(path_AdminElevateword_AdminElevatewordEditJsp).useForm(CreateForm.class, op -> {
200             op.setup(form -> {
201                 form.initialize();
202                 form.crudMode = CrudMode.CREATE;
203             });
204         }).renderWith(data -> {
205             registerLabels(data);
206         });
207     }
208 
209     /**
210      * Shows the form for editing an existing elevate word.
211      *
212      * @param form edit form containing the elevate word ID
213      * @return HTML response for the edit elevate word form
214      */
215     @Execute
216     @Secured({ ROLE })
217     public HtmlResponse edit(final EditForm form) {
218         validate(form, messages -> {}, this::asListHtml);
219         final String id = form.id;
220         elevateWordService.getElevateWord(id).ifPresent(entity -> {
221             copyBeanToBean(entity, form, copyOp -> {
222                 copyOp.excludeNull();
223                 copyOp.exclude(Constants.PERMISSIONS);
224             });
225             final PermissionHelper permissionHelper = ComponentUtil.getPermissionHelper();
226             form.permissions = stream(entity.getPermissions()).get(stream -> stream.map(s -> permissionHelper.decode(s))
227                     .filter(StringUtil::isNotBlank)
228                     .distinct()
229                     .collect(Collectors.joining("\n")));
230         }).orElse(() -> {
231             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, id), this::asListHtml);
232         });
233         saveToken();
234         if (form.crudMode.intValue() == CrudMode.EDIT) {
235             // back
236             form.crudMode = CrudMode.DETAILS;
237             return asDetailsHtml();
238         }
239         form.crudMode = CrudMode.EDIT;
240         return asEditHtml();
241     }
242 
243     // -----------------------------------------------------
244     //                                               Details
245     //                                               -------
246     /**
247      * Shows detailed information for a specific elevate word.
248      *
249      * @param crudMode CRUD operation mode
250      * @param id unique identifier of the elevate word
251      * @return HTML response with elevate word details
252      */
253     @Execute
254     @Secured({ ROLE, ROLE + VIEW })
255     public HtmlResponse details(final int crudMode, final String id) {
256         verifyCrudMode(crudMode, CrudMode.DETAILS, this::asListHtml);
257         saveToken();
258         final PermissionHelper permissionHelper = ComponentUtil.getPermissionHelper();
259         return asHtml(path_AdminElevateword_AdminElevatewordDetailsJsp).useForm(EditForm.class, op -> op.setup(form -> {
260             elevateWordService.getElevateWord(id).ifPresent(entity -> {
261                 copyBeanToBean(entity, form, copyOp -> {
262                     copyOp.excludeNull();
263                     copyOp.exclude(Constants.PERMISSIONS);
264                 });
265                 form.permissions = stream(entity.getPermissions()).get(stream -> stream.map(permissionHelper::decode)
266                         .filter(StringUtil::isNotBlank)
267                         .distinct()
268                         .collect(Collectors.joining("\n")));
269                 form.crudMode = crudMode;
270             }).orElse(() -> throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, id), this::asListHtml));
271         })).renderWith(this::registerLabels);
272     }
273 
274     // -----------------------------------------------------
275     //                                              Download
276     //                                               -------
277     /**
278      * Shows the download page for exporting elevate words.
279      *
280      * @return HTML response for the download page
281      */
282     @Execute
283     @Secured({ ROLE, ROLE + VIEW })
284     public HtmlResponse downloadpage() {
285         saveToken();
286         return asDownloadHtml();
287     }
288 
289     /**
290      * Downloads elevate words data as a CSV file.
291      *
292      * @param form download form with export options
293      * @return streaming response containing CSV data
294      */
295     @Execute
296     @Secured({ ROLE, ROLE + VIEW })
297     public ActionResponse download(final DownloadForm form) {
298         verifyToken(this::asDownloadHtml);
299 
300         return asStream("elevate.csv").contentTypeOctetStream().stream(out -> {
301             final Path tempFile = ComponentUtil.getSystemHelper().createTempFile("fess-elevate-", ".csv").toPath();
302             try {
303                 try (Writer writer = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(tempFile), getCsvEncoding()))) {
304                     elevateWordService.exportCsv(writer);
305                 } catch (final Exception e) {
306                     logger.warn("Failed to process a request.", e);
307                     throwValidationError(messages -> messages.addErrorsFailedToDownloadElevateFile(GLOBAL), this::asDownloadHtml);
308                 }
309                 try (InputStream in = Files.newInputStream(tempFile)) {
310                     out.write(in);
311                 }
312             } finally {
313                 Files.delete(tempFile);
314             }
315         });
316     }
317 
318     // -----------------------------------------------------
319     //                                                Upload
320     //                                               -------
321     /**
322      * Shows the upload page for importing elevate words.
323      *
324      * @return HTML response for the upload page
325      */
326     @Execute
327     @Secured({ ROLE })
328     public HtmlResponse uploadpage() {
329         saveToken();
330         return asUploadHtml();
331     }
332 
333     // -----------------------------------------------------
334     //                                         Actually Crud
335     //                                         -------------
336     /**
337      * Creates a new elevate word entry.
338      *
339      * @param form create form containing new elevate word data
340      * @return redirect response to the main elevate word page
341      */
342     @Execute
343     @Secured({ ROLE })
344     public HtmlResponse create(final CreateForm form) {
345         verifyCrudMode(form.crudMode, CrudMode.CREATE, this::asListHtml);
346         validate(form, messages -> {}, this::asEditHtml);
347         verifyToken(this::asEditHtml);
348         getElevateWord(form).ifPresent(entity -> {
349             try {
350                 elevateWordService.store(entity);
351                 suggestHelper.addElevateWord(entity.getSuggestWord(), entity.getReading(), entity.getLabelTypeValues(),
352                         entity.getPermissions(), entity.getBoost(), false);
353                 saveInfo(messages -> messages.addSuccessCrudCreateCrudTable(GLOBAL));
354             } catch (final Exception e) {
355                 logger.warn("Failed to process a request.", e);
356                 throwValidationError(messages -> messages.addErrorsCrudFailedToCreateCrudTable(GLOBAL, buildThrowableMessage(e)),
357                         this::asEditHtml);
358             }
359         }).orElse(() -> {
360             throwValidationError(messages -> messages.addErrorsCrudFailedToCreateInstance(GLOBAL), this::asEditHtml);
361         });
362         return redirect(getClass());
363     }
364 
365     /**
366      * Updates an existing elevate word entry.
367      *
368      * @param form edit form containing updated elevate word data
369      * @return redirect response to the main elevate word page
370      */
371     @Execute
372     @Secured({ ROLE })
373     public HtmlResponse update(final EditForm form) {
374         verifyCrudMode(form.crudMode, CrudMode.EDIT, this::asListHtml);
375         validate(form, messages -> {}, this::asEditHtml);
376         verifyToken(this::asEditHtml);
377         getElevateWord(form).ifPresent(entity -> {
378             try {
379                 elevateWordService.store(entity);
380                 suggestHelper.deleteAllElevateWord(false);
381                 suggestHelper.storeAllElevateWords(false);
382                 saveInfo(messages -> messages.addSuccessCrudUpdateCrudTable(GLOBAL));
383             } catch (final Exception e) {
384                 logger.warn("Failed to process a request.", e);
385                 throwValidationError(messages -> messages.addErrorsCrudFailedToUpdateCrudTable(GLOBAL, buildThrowableMessage(e)),
386                         this::asEditHtml);
387             }
388         }).orElse(() -> {
389             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, form.id), this::asEditHtml);
390         });
391         return redirect(getClass());
392     }
393 
394     /**
395      * Deletes an existing elevate word entry.
396      *
397      * @param form edit form containing the elevate word ID to delete
398      * @return redirect response to the main elevate word page
399      */
400     @Execute
401     @Secured({ ROLE })
402     public HtmlResponse delete(final EditForm form) {
403         verifyCrudMode(form.crudMode, CrudMode.DETAILS, this::asListHtml);
404         validate(form, messages -> {}, this::asDetailsHtml);
405         verifyToken(this::asDetailsHtml);
406         final String id = form.id;
407         elevateWordService.getElevateWord(id).ifPresent(entity -> {
408             try {
409                 elevateWordService.delete(entity);
410                 suggestHelper.deleteElevateWord(entity.getSuggestWord(), false);
411                 saveInfo(messages -> messages.addSuccessCrudDeleteCrudTable(GLOBAL));
412             } catch (final Exception e) {
413                 logger.warn("Failed to process a request.", e);
414                 throwValidationError(messages -> messages.addErrorsCrudFailedToDeleteCrudTable(GLOBAL, buildThrowableMessage(e)),
415                         this::asEditHtml);
416             }
417         }).orElse(() -> throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, id), this::asDetailsHtml));
418         return redirect(getClass());
419     }
420 
421     /**
422      * Uploads and imports elevate words from a CSV file.
423      *
424      * @param form upload form containing the CSV file
425      * @return redirect response to the main elevate word page
426      */
427     @Execute
428     @Secured({ ROLE })
429     public HtmlResponse upload(final UploadForm form) {
430         validate(form, messages -> {}, this::asUploadHtml);
431         verifyToken(this::asUploadHtml);
432         CommonPoolUtil.execute(() -> {
433             try (Reader reader = new BufferedReader(new InputStreamReader(form.elevateWordFile.getInputStream(), getCsvEncoding()))) {
434                 elevateWordService.importCsv(reader);
435                 suggestHelper.deleteAllElevateWord(false);
436                 suggestHelper.storeAllElevateWords(false);
437             } catch (final Exception e) {
438                 throw new FessSystemException("Failed to import elevate word data: fileName=" + form.elevateWordFile.getFileName(), e);
439             }
440         });
441         saveInfo(messages -> messages.addSuccessUploadElevateWord(GLOBAL));
442         return redirect(getClass());
443     }
444 
445     // ===================================================================================
446     //                                                                        Assist Logic
447     //                                                                        ============
448     /**
449      * Creates an ElevateWord entity based on the form and operation mode.
450      *
451      * @param form create form containing elevate word data
452      * @param username current user's username
453      * @param currentTime current timestamp
454      * @return optional ElevateWord entity
455      */
456     public static OptionalEntity<ElevateWord> getEntity(final CreateForm form, final String username, final long currentTime) {
457         switch (form.crudMode) {
458         case CrudMode.CREATE:
459             return OptionalEntity.of(new ElevateWord()).map(entity -> {
460                 entity.setCreatedBy(username);
461                 entity.setCreatedTime(currentTime);
462                 return entity;
463             });
464         case CrudMode.EDIT:
465             if (form instanceof EditForm) {
466                 return ComponentUtil.getComponent(ElevateWordService.class).getElevateWord(((EditForm) form).id);
467             }
468             break;
469         default:
470             break;
471         }
472         return OptionalEntity.empty();
473     }
474 
475     /**
476      * Creates and populates an ElevateWord entity from form data.
477      *
478      * @param form create form containing elevate word data
479      * @return optional ElevateWord entity with populated data
480      */
481     public static OptionalEntity<ElevateWord> getElevateWord(final CreateForm form) {
482         final SystemHelper systemHelper = ComponentUtil.getSystemHelper();
483         final String username = systemHelper.getUsername();
484         final long currentTime = systemHelper.getCurrentTimeAsLong();
485 
486         return getEntity(form, username, currentTime).map(entity -> {
487             entity.setUpdatedBy(username);
488             entity.setUpdatedTime(currentTime);
489             BeanUtil.copyBeanToBean(form, entity,
490                     op -> op.exclude(Stream.concat(Stream.of(Constants.COMMON_CONVERSION_RULE), Stream.of(Constants.PERMISSIONS))
491                             .toArray(n -> new String[n])));
492             final PermissionHelper permissionHelper = ComponentUtil.getPermissionHelper();
493             entity.setPermissions(split(form.permissions, "\n").get(
494                     stream -> stream.map(permissionHelper::encode).filter(StringUtil::isNotBlank).distinct().toArray(n -> new String[n])));
495             return entity;
496         });
497     }
498 
499     /**
500      * Registers label type data for rendering in the view.
501      *
502      * @param data render data to populate with label information
503      */
504     protected void registerLabels(final RenderData data) {
505         RenderDataUtil.register(data, "labelTypeItems", labelTypeService.getLabelTypeList());
506     }
507 
508     // ===================================================================================
509     //                                                                        Small Helper
510     //                                                                        ============
511     private String getCsvEncoding() {
512         return fessConfig.getCsvFileEncoding();
513     }
514 
515     // ===================================================================================
516     //                                                                              JSP
517     //                                                                           =========
518     private HtmlResponse asListHtml() {
519         return asHtml(path_AdminElevateword_AdminElevatewordJsp).renderWith(data -> {
520             RenderDataUtil.register(data, "elevateWordItems", elevateWordService.getElevateWordList(elevateWordPager)); // page navi
521         }).useForm(SearchForm.class, setup -> {
522             setup.setup(form -> {
523                 copyBeanToBean(elevateWordPager, form, op -> op.include("id"));
524             });
525         });
526     }
527 
528     private HtmlResponse asEditHtml() {
529         return asHtml(path_AdminElevateword_AdminElevatewordEditJsp).renderWith(data -> {
530             registerLabels(data);
531         });
532     }
533 
534     private HtmlResponse asDetailsHtml() {
535         return asHtml(path_AdminElevateword_AdminElevatewordDetailsJsp).renderWith(data -> {
536             registerLabels(data);
537         });
538     }
539 
540     private HtmlResponse asUploadHtml() {
541         return asHtml(path_AdminElevateword_AdminElevatewordUploadJsp).useForm(UploadForm.class);
542     }
543 
544     private HtmlResponse asDownloadHtml() {
545         return asHtml(path_AdminElevateword_AdminElevatewordDownloadJsp).useForm(DownloadForm.class);
546     }
547 }