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.fileconfig;
17  
18  import static org.codelibs.core.stream.StreamUtil.split;
19  import static org.codelibs.core.stream.StreamUtil.stream;
20  
21  import java.util.stream.Collectors;
22  import java.util.stream.Stream;
23  
24  import org.apache.logging.log4j.LogManager;
25  import org.apache.logging.log4j.Logger;
26  import org.codelibs.core.lang.StringUtil;
27  import org.codelibs.fess.Constants;
28  import org.codelibs.fess.annotation.Secured;
29  import org.codelibs.fess.app.pager.FileConfigPager;
30  import org.codelibs.fess.app.service.FileConfigService;
31  import org.codelibs.fess.app.service.LabelTypeService;
32  import org.codelibs.fess.app.service.RoleTypeService;
33  import org.codelibs.fess.app.web.CrudMode;
34  import org.codelibs.fess.app.web.base.FessAdminAction;
35  import org.codelibs.fess.helper.PermissionHelper;
36  import org.codelibs.fess.helper.SystemHelper;
37  import org.codelibs.fess.opensearch.config.exentity.CrawlingConfig.ConfigType;
38  import org.codelibs.fess.opensearch.config.exentity.FileConfig;
39  import org.codelibs.fess.util.ComponentUtil;
40  import org.codelibs.fess.util.RenderDataUtil;
41  import org.dbflute.optional.OptionalEntity;
42  import org.dbflute.optional.OptionalThing;
43  import org.lastaflute.web.Execute;
44  import org.lastaflute.web.response.HtmlResponse;
45  import org.lastaflute.web.response.render.RenderData;
46  import org.lastaflute.web.ruts.process.ActionRuntime;
47  
48  import jakarta.annotation.Resource;
49  
50  /**
51   * Admin action for File Config management.
52   *
53   */
54  public class AdminFileconfigAction extends FessAdminAction {
55  
56      /**
57       * Default constructor.
58       */
59      public AdminFileconfigAction() {
60          super();
61      }
62  
63      /** The role name for file configuration administration. */
64      public static final String ROLE = "admin-fileconfig";
65  
66      /** Logger for this class. */
67      private static final Logger logger = LogManager.getLogger(AdminFileconfigAction.class);
68  
69      // ===================================================================================
70      //                                                                           Attribute
71      //                                                                           =========
72      /** Service for file configuration operations. */
73      @Resource
74      private FileConfigService fileConfigService;
75  
76      /** Pager for file configuration list pagination. */
77      @Resource
78      private FileConfigPager fileConfigPager;
79  
80      /** Service for role type operations. */
81      @Resource
82      private RoleTypeService roleTypeService;
83  
84      /** Service for label type operations. */
85      @Resource
86      private LabelTypeService labelTypeService;
87  
88      // ===================================================================================
89      //                                                                               Hook
90      //                                                                              ======
91      /**
92       * Sets up HTML data for rendering, including help link.
93       *
94       * @param runtime the action runtime
95       */
96      @Override
97      protected void setupHtmlData(final ActionRuntime runtime) {
98          super.setupHtmlData(runtime);
99          runtime.registerData("helpLink", systemHelper.getHelpLink(fessConfig.getOnlineHelpNameFileconfig()));
100     }
101 
102     /**
103      * Returns the action role for this admin action.
104      *
105      * @return the role name
106      */
107     @Override
108     protected String getActionRole() {
109         return ROLE;
110     }
111 
112     // ===================================================================================
113     //                                                                      Search Execute
114     //                                                                      ==============
115     /**
116      * Displays the file configuration list page.
117      *
118      * @return HTML response for the list page
119      */
120     @Execute
121     @Secured({ ROLE, ROLE + VIEW })
122     public HtmlResponse index() {
123         return asListHtml();
124     }
125 
126     /**
127      * Displays the file configuration list with pagination.
128      *
129      * @param pageNumber the page number
130      * @param form the search form
131      * @return HTML response for the list page
132      */
133     @Execute
134     @Secured({ ROLE, ROLE + VIEW })
135     public HtmlResponse list(final OptionalThing<Integer> pageNumber, final SearchForm form) {
136         pageNumber.ifPresent(num -> {
137             fileConfigPager.setCurrentPageNumber(pageNumber.get());
138         }).orElse(() -> {
139             fileConfigPager.setCurrentPageNumber(0);
140         });
141         return asHtml(path_AdminFileconfig_AdminFileconfigJsp).renderWith(data -> {
142             searchPaging(data, form);
143         });
144     }
145 
146     /**
147      * Searches file configurations based on the form criteria.
148      *
149      * @param form the search form
150      * @return HTML response for the search results
151      */
152     @Execute
153     @Secured({ ROLE, ROLE + VIEW })
154     public HtmlResponse search(final SearchForm form) {
155         copyBeanToBean(form, fileConfigPager, op -> op.exclude(Constants.PAGER_CONVERSION_RULE));
156         return asHtml(path_AdminFileconfig_AdminFileconfigJsp).renderWith(data -> {
157             searchPaging(data, form);
158         });
159     }
160 
161     /**
162      * Resets the search criteria and displays the default list.
163      *
164      * @param form the search form
165      * @return HTML response for the reset list
166      */
167     @Execute
168     @Secured({ ROLE, ROLE + VIEW })
169     public HtmlResponse reset(final SearchForm form) {
170         fileConfigPager.clear();
171         return asHtml(path_AdminFileconfig_AdminFileconfigJsp).renderWith(data -> {
172             searchPaging(data, form);
173         });
174     }
175 
176     /**
177      * Sets up data for search result pagination.
178      *
179      * @param data the render data
180      * @param form the search form
181      */
182     protected void searchPaging(final RenderData data, final SearchForm form) {
183         RenderDataUtil.register(data, "fileConfigItems", fileConfigService.getFileConfigList(fileConfigPager)); // page navi
184 
185         // restore from pager
186         copyBeanToBean(fileConfigPager, form, op -> op.include("name", "paths", "description"));
187     }
188 
189     // ===================================================================================
190     //                                                                        Edit Execute
191     //                                                                        ============
192     // -----------------------------------------------------
193     //                                            Entry Page
194     //                                            ----------
195     /**
196      * Displays the create new file configuration page.
197      *
198      * @return HTML response for the create page
199      */
200     @Execute
201     @Secured({ ROLE })
202     public HtmlResponse createnew() {
203         saveToken();
204         return asHtml(path_AdminFileconfig_AdminFileconfigEditJsp).useForm(CreateForm.class, op -> {
205             op.setup(form -> {
206                 form.initialize();
207                 ComponentUtil.getCrawlingConfigHelper().getDefaultConfig(ConfigType.FILE).ifPresent(entity -> {
208                     copyBeanToBean(entity, form, copyOp -> {
209                         copyOp.excludeNull();
210                         copyOp.exclude(Stream.concat(Stream.of(Constants.COMMON_CONVERSION_RULE),
211                                 Stream.of(Constants.PERMISSIONS, Constants.VIRTUAL_HOSTS)).toArray(n -> new String[n]));
212                     });
213                     final PermissionHelper permissionHelper = ComponentUtil.getPermissionHelper();
214                     form.permissions = stream(entity.getPermissions()).get(stream -> stream.map(s -> permissionHelper.decode(s))
215                             .filter(StringUtil::isNotBlank)
216                             .distinct()
217                             .collect(Collectors.joining("\n")));
218                     form.virtualHosts = stream(entity.getVirtualHosts())
219                             .get(stream -> stream.filter(StringUtil::isNotBlank).map(String::trim).collect(Collectors.joining("\n")));
220                     form.name = null;
221                 });
222                 form.crudMode = CrudMode.CREATE;
223             });
224         }).renderWith(data -> {
225             registerRolesAndLabels(data);
226         });
227     }
228 
229     /**
230      * Displays the form for duplicating an existing file crawler configuration.
231      *
232      * @param id the ID of the file config to duplicate
233      * @return HTML response for the file config creation form pre-populated with duplicated values
234      */
235     @Execute
236     @Secured({ ROLE })
237     public HtmlResponse duplicate(final String id) {
238         saveToken();
239         return asHtml(path_AdminFileconfig_AdminFileconfigEditJsp).useForm(CreateForm.class, op -> {
240             op.setup(form -> {
241                 form.initialize();
242                 fileConfigService.getFileConfig(id).ifPresent(entity -> {
243                     copyBeanToBean(entity, form, copyOp -> {
244                         copyOp.excludeNull();
245                         copyOp.exclude(Stream.concat(Stream.of(Constants.COMMON_CONVERSION_RULE),
246                                 Stream.of(Constants.PERMISSIONS, Constants.VIRTUAL_HOSTS)).toArray(n -> new String[n]));
247                     });
248                     final PermissionHelper permissionHelper = ComponentUtil.getPermissionHelper();
249                     form.permissions = stream(entity.getPermissions()).get(stream -> stream.map(s -> permissionHelper.decode(s))
250                             .filter(StringUtil::isNotBlank)
251                             .distinct()
252                             .collect(Collectors.joining("\n")));
253                     form.virtualHosts = stream(entity.getVirtualHosts())
254                             .get(stream -> stream.filter(StringUtil::isNotBlank).map(String::trim).collect(Collectors.joining("\n")));
255                     form.name = null;
256                 }).orElse(() -> {
257                     throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, id), this::asListHtml);
258                 });
259                 form.crudMode = CrudMode.CREATE;
260             });
261         }).renderWith(data -> {
262             registerRolesAndLabels(data);
263         });
264     }
265 
266     /**
267      * Displays the edit file configuration page.
268      *
269      * @param form the edit form
270      * @return HTML response for the edit page
271      */
272     @Execute
273     @Secured({ ROLE })
274     public HtmlResponse edit(final EditForm form) {
275         validate(form, messages -> {}, this::asListHtml);
276         final PermissionHelper permissionHelper = ComponentUtil.getPermissionHelper();
277         final String id = form.id;
278         fileConfigService.getFileConfig(id).ifPresent(entity -> {
279             copyBeanToBean(entity, form, copyOp -> {
280                 copyOp.excludeNull();
281                 copyOp.exclude(Constants.PERMISSIONS, Constants.VIRTUAL_HOSTS);
282             });
283             form.permissions = stream(entity.getPermissions()).get(stream -> stream.map(permissionHelper::decode)
284                     .filter(StringUtil::isNotBlank)
285                     .distinct()
286                     .collect(Collectors.joining("\n")));
287             form.virtualHosts = stream(entity.getVirtualHosts())
288                     .get(stream -> stream.filter(StringUtil::isNotBlank).map(String::trim).collect(Collectors.joining("\n")));
289         }).orElse(() -> throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, id), this::asListHtml));
290         saveToken();
291         if (form.crudMode.intValue() == CrudMode.EDIT) {
292             // back
293             form.crudMode = CrudMode.DETAILS;
294             return asDetailsHtml();
295         }
296         form.crudMode = CrudMode.EDIT;
297         return asEditHtml();
298     }
299 
300     // -----------------------------------------------------
301     //                                               Details
302     //                                               -------
303     /**
304      * Displays the file configuration details page.
305      *
306      * @param crudMode the CRUD mode
307      * @param id the file configuration ID
308      * @return HTML response for the details page
309      */
310     @Execute
311     @Secured({ ROLE, ROLE + VIEW })
312     public HtmlResponse details(final int crudMode, final String id) {
313         verifyCrudMode(crudMode, CrudMode.DETAILS, this::asListHtml);
314         saveToken();
315         return asHtml(path_AdminFileconfig_AdminFileconfigDetailsJsp).useForm(EditForm.class, op -> op.setup(form -> {
316             fileConfigService.getFileConfig(id).ifPresent(entity -> {
317                 copyBeanToBean(entity, form, copyOp -> {
318                     copyOp.excludeNull();
319                     copyOp.exclude(Constants.PERMISSIONS, Constants.VIRTUAL_HOSTS);
320                 });
321                 final PermissionHelper permissionHelper = ComponentUtil.getPermissionHelper();
322                 form.permissions = stream(entity.getPermissions()).get(stream -> stream.map(s -> permissionHelper.decode(s))
323                         .filter(StringUtil::isNotBlank)
324                         .distinct()
325                         .collect(Collectors.joining("\n")));
326                 form.virtualHosts = stream(entity.getVirtualHosts())
327                         .get(stream -> stream.filter(StringUtil::isNotBlank).map(String::trim).collect(Collectors.joining("\n")));
328                 form.crudMode = crudMode;
329             }).orElse(() -> throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, id), this::asListHtml));
330         })).renderWith(this::registerRolesAndLabels);
331     }
332 
333     // -----------------------------------------------------
334     //                                         Actually Crud
335     //                                         -------------
336     /**
337      * Creates a new file configuration.
338      *
339      * @param form the create form
340      * @return HTML response after creation
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         getFileConfig(form).ifPresent(entity -> {
349             try {
350                 fileConfigService.store(entity);
351                 saveInfo(messages -> messages.addSuccessCrudCreateCrudTable(GLOBAL));
352             } catch (final Exception e) {
353                 logger.warn("Failed to process a request.", e);
354                 throwValidationError(messages -> messages.addErrorsCrudFailedToCreateCrudTable(GLOBAL, buildThrowableMessage(e)),
355                         this::asEditHtml);
356             }
357         }).orElse(() -> {
358             throwValidationError(messages -> messages.addErrorsCrudFailedToCreateInstance(GLOBAL), this::asEditHtml);
359         });
360         return redirect(getClass());
361     }
362 
363     /**
364      * Updates an existing file configuration.
365      *
366      * @param form the edit form
367      * @return HTML response after update
368      */
369     @Execute
370     @Secured({ ROLE })
371     public HtmlResponse update(final EditForm form) {
372         verifyCrudMode(form.crudMode, CrudMode.EDIT, this::asListHtml);
373         validate(form, messages -> {}, this::asEditHtml);
374         verifyToken(this::asEditHtml);
375         getFileConfig(form).ifPresent(entity -> {
376             try {
377                 fileConfigService.store(entity);
378                 saveInfo(messages -> messages.addSuccessCrudUpdateCrudTable(GLOBAL));
379             } catch (final Exception e) {
380                 logger.warn("Failed to process a request.", e);
381                 throwValidationError(messages -> messages.addErrorsCrudFailedToUpdateCrudTable(GLOBAL, buildThrowableMessage(e)),
382                         this::asEditHtml);
383             }
384         }).orElse(() -> {
385             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, form.id), this::asEditHtml);
386         });
387         return redirect(getClass());
388     }
389 
390     /**
391      * Deletes a file configuration.
392      *
393      * @param form the edit form
394      * @return HTML response after deletion
395      */
396     @Execute
397     @Secured({ ROLE })
398     public HtmlResponse delete(final EditForm form) {
399         verifyCrudMode(form.crudMode, CrudMode.DETAILS, this::asListHtml);
400         validate(form, messages -> {}, this::asDetailsHtml);
401         verifyToken(this::asDetailsHtml);
402         final String id = form.id;
403         fileConfigService.getFileConfig(id).ifPresent(entity -> {
404             try {
405                 fileConfigService.delete(entity);
406                 saveInfo(messages -> messages.addSuccessCrudDeleteCrudTable(GLOBAL));
407             } catch (final Exception e) {
408                 logger.warn("Failed to process a request.", e);
409                 throwValidationError(messages -> messages.addErrorsCrudFailedToDeleteCrudTable(GLOBAL, buildThrowableMessage(e)),
410                         this::asEditHtml);
411             }
412         }).orElse(() -> {
413             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, id), this::asDetailsHtml);
414         });
415         return redirect(getClass());
416     }
417 
418     // ===================================================================================
419     //                                                                        Assist Logic
420     //                                                                        ============
421     /**
422      * Gets a file configuration entity based on the form and current user info.
423      *
424      * @param form the create form
425      * @param username the current username
426      * @param currentTime the current time
427      * @return optional file configuration entity
428      */
429     public static OptionalEntity<FileConfig> getEntity(final CreateForm form, final String username, final long currentTime) {
430         switch (form.crudMode) {
431         case CrudMode.CREATE:
432             return OptionalEntity.of(new FileConfig()).map(entity -> {
433                 entity.setCreatedBy(username);
434                 entity.setCreatedTime(currentTime);
435                 return entity;
436             });
437         case CrudMode.EDIT:
438             if (form instanceof EditForm) {
439                 return ComponentUtil.getComponent(FileConfigService.class).getFileConfig(((EditForm) form).id);
440             }
441             break;
442         default:
443             break;
444         }
445         return OptionalEntity.empty();
446     }
447 
448     /**
449      * Gets a file configuration entity from the form with system info.
450      *
451      * @param form the create form
452      * @return optional file configuration entity
453      */
454     public static OptionalEntity<FileConfig> getFileConfig(final CreateForm form) {
455         final SystemHelper systemHelper = ComponentUtil.getSystemHelper();
456         final String username = systemHelper.getUsername();
457         final long currentTime = systemHelper.getCurrentTimeAsLong();
458         return getEntity(form, username, currentTime).map(entity -> {
459             entity.setUpdatedBy(username);
460             entity.setUpdatedTime(currentTime);
461             copyBeanToBean(form, entity,
462                     op -> op.exclude(Stream
463                             .concat(Stream.of(Constants.COMMON_CONVERSION_RULE), Stream.of(Constants.PERMISSIONS, Constants.VIRTUAL_HOSTS))
464                             .toArray(n -> new String[n])));
465             entity.setPermissions(encodePermissions(form.permissions));
466             entity.setVirtualHosts(split(form.virtualHosts, "\n")
467                     .get(stream -> stream.filter(StringUtil::isNotBlank).distinct().map(String::trim).toArray(n -> new String[n])));
468             return entity;
469         });
470     }
471 
472     /**
473      * Registers roles and labels for the dropdown lists.
474      *
475      * @param data the render data
476      */
477     protected void registerRolesAndLabels(final RenderData data) {
478         RenderDataUtil.register(data, "labelSettingEnabled", fessConfig.isFormAdminLabelInConfigEnabled());
479         RenderDataUtil.register(data, "roleTypeItems", roleTypeService.getRoleTypeList());
480         RenderDataUtil.register(data, "labelTypeItems", labelTypeService.getLabelTypeList());
481     }
482 
483     // ===================================================================================
484     //                                                                        Small Helper
485     //                                                                        ============
486     //                                                                              JSP
487     //                                                                           =========
488 
489     /**
490      * Returns HTML response for the list page.
491      *
492      * @return HTML response for the list page
493      */
494     private HtmlResponse asListHtml() {
495         return asHtml(path_AdminFileconfig_AdminFileconfigJsp).renderWith(data -> {
496             RenderDataUtil.register(data, "fileConfigItems", fileConfigService.getFileConfigList(fileConfigPager)); // page navi
497         }).useForm(SearchForm.class, setup -> {
498             setup.setup(form -> {
499                 copyBeanToBean(fileConfigPager, form, op -> op.include("name", "paths", "description"));
500             });
501         });
502     }
503 
504     /**
505      * Returns HTML response for the edit page.
506      *
507      * @return HTML response for the edit page
508      */
509     private HtmlResponse asEditHtml() {
510         return asHtml(path_AdminFileconfig_AdminFileconfigEditJsp).renderWith(data -> {
511             registerRolesAndLabels(data);
512         });
513     }
514 
515     /**
516      * Returns HTML response for the details page.
517      *
518      * @return HTML response for the details page
519      */
520     private HtmlResponse asDetailsHtml() {
521         return asHtml(path_AdminFileconfig_AdminFileconfigDetailsJsp).renderWith(data -> {
522             registerRolesAndLabels(data);
523         });
524     }
525 }