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