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