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.group;
17  
18  import java.util.Base64;
19  import java.util.Map;
20  import java.util.function.Consumer;
21  
22  import org.apache.logging.log4j.LogManager;
23  import org.apache.logging.log4j.Logger;
24  import org.codelibs.fess.Constants;
25  import org.codelibs.fess.annotation.Secured;
26  import org.codelibs.fess.app.pager.GroupPager;
27  import org.codelibs.fess.app.service.GroupService;
28  import org.codelibs.fess.app.web.CrudMode;
29  import org.codelibs.fess.app.web.base.FessAdminAction;
30  import org.codelibs.fess.mylasta.action.FessMessages;
31  import org.codelibs.fess.opensearch.user.exentity.Group;
32  import org.codelibs.fess.util.ComponentUtil;
33  import org.codelibs.fess.util.RenderDataUtil;
34  import org.dbflute.optional.OptionalEntity;
35  import org.dbflute.optional.OptionalThing;
36  import org.lastaflute.web.Execute;
37  import org.lastaflute.web.response.HtmlResponse;
38  import org.lastaflute.web.response.render.RenderData;
39  import org.lastaflute.web.ruts.process.ActionRuntime;
40  import org.lastaflute.web.validation.VaMessenger;
41  
42  import jakarta.annotation.Resource;
43  
44  /**
45   * Admin action for Group management.
46   *
47   */
48  public class AdminGroupAction extends FessAdminAction {
49  
50      /**
51       * Default constructor.
52       */
53      public AdminGroupAction() {
54          super();
55      }
56  
57      /** The role name for group administration. */
58      public static final String ROLE = "admin-group";
59  
60      /** Logger for this class. */
61      private static final Logger logger = LogManager.getLogger(AdminGroupAction.class);
62  
63      // ===================================================================================
64      //                                                                           Attribute
65      //                                                                           =========
66      /** Service for group operations. */
67      @Resource
68      private GroupService groupService;
69  
70      /** Pager for group list pagination. */
71      @Resource
72      private GroupPager groupPager;
73  
74      // ===================================================================================
75      //                                                                               Hook
76      //                                                                              ======
77      /**
78       * Sets up HTML data for rendering, including help link.
79       *
80       * @param runtime the action runtime
81       */
82      @Override
83      protected void setupHtmlData(final ActionRuntime runtime) {
84          super.setupHtmlData(runtime);
85          runtime.registerData("helpLink", systemHelper.getHelpLink(fessConfig.getOnlineHelpNameGroup()));
86      }
87  
88      /**
89       * Returns the action role for this admin action.
90       *
91       * @return the role name
92       */
93      @Override
94      protected String getActionRole() {
95          return ROLE;
96      }
97  
98      // ===================================================================================
99      //                                                                      Search Execute
100     //                                                                      ==============
101     /**
102      * Displays the group list page.
103      *
104      * @return HTML response for the list page
105      */
106     @Execute
107     @Secured({ ROLE, ROLE + VIEW })
108     public HtmlResponse index() {
109         return asListHtml();
110     }
111 
112     /**
113      * Displays the group list with pagination.
114      *
115      * @param pageNumber the page number
116      * @param form the search form
117      * @return HTML response for the list page
118      */
119     @Execute
120     @Secured({ ROLE, ROLE + VIEW })
121     public HtmlResponse list(final OptionalThing<Integer> pageNumber, final SearchForm form) {
122         pageNumber.ifPresent(num -> {
123             groupPager.setCurrentPageNumber(pageNumber.get());
124         }).orElse(() -> {
125             groupPager.setCurrentPageNumber(0);
126         });
127         return asHtml(path_AdminGroup_AdminGroupJsp).renderWith(data -> {
128             searchPaging(data, form);
129         });
130     }
131 
132     /**
133      * Searches groups based on the form criteria.
134      *
135      * @param form the search form
136      * @return HTML response for the search results
137      */
138     @Execute
139     @Secured({ ROLE, ROLE + VIEW })
140     public HtmlResponse search(final SearchForm form) {
141         copyBeanToBean(form, groupPager, op -> op.exclude(Constants.PAGER_CONVERSION_RULE));
142         return asHtml(path_AdminGroup_AdminGroupJsp).renderWith(data -> {
143             searchPaging(data, form);
144         });
145     }
146 
147     /**
148      * Resets the search criteria and displays the default list.
149      *
150      * @param form the search form
151      * @return HTML response for the reset list
152      */
153     @Execute
154     @Secured({ ROLE, ROLE + VIEW })
155     public HtmlResponse reset(final SearchForm form) {
156         groupPager.clear();
157         return asHtml(path_AdminGroup_AdminGroupJsp).renderWith(data -> {
158             searchPaging(data, form);
159         });
160     }
161 
162     /**
163      * Sets up data for search result pagination.
164      *
165      * @param data the render data
166      * @param form the search form
167      */
168     protected void searchPaging(final RenderData data, final SearchForm form) {
169         RenderDataUtil.register(data, "groupItems", groupService.getGroupList(groupPager)); // page navi
170 
171         // restore from pager
172         copyBeanToBean(groupPager, form, op -> op.include("id"));
173     }
174 
175     // ===================================================================================
176     //                                                                        Edit Execute
177     //                                                                        ============
178     // -----------------------------------------------------
179     //                                            Entry Page
180     //                                            ----------
181     /**
182      * Displays the create new group page.
183      *
184      * @return HTML response for the create page
185      */
186     @Execute
187     @Secured({ ROLE })
188     public HtmlResponse createnew() {
189         saveToken();
190         return asHtml(path_AdminGroup_AdminGroupEditJsp).useForm(CreateForm.class, op -> {
191             op.setup(form -> {
192                 form.initialize();
193                 form.crudMode = CrudMode.CREATE;
194             });
195         });
196     }
197 
198     /**
199      * Displays the edit group page.
200      *
201      * @param form the edit form
202      * @return HTML response for the edit page
203      */
204     @Execute
205     @Secured({ ROLE })
206     public HtmlResponse edit(final EditForm form) {
207         validate(form, messages -> {}, this::asListHtml);
208         final String id = form.id;
209         groupService.getGroup(id).ifPresent(entity -> {
210             copyBeanToBean(entity, form, op -> {});
211         }).orElse(() -> {
212             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, id), this::asListHtml);
213         });
214         saveToken();
215         if (form.crudMode.intValue() == CrudMode.EDIT) {
216             // back
217             form.crudMode = CrudMode.DETAILS;
218             return asDetailsHtml();
219         }
220         form.crudMode = CrudMode.EDIT;
221         return asEditHtml();
222     }
223 
224     // -----------------------------------------------------
225     //                                               Details
226     //                                               -------
227     /**
228      * Displays the group details page.
229      *
230      * @param crudMode the CRUD mode
231      * @param id the group ID
232      * @return HTML response for the details page
233      */
234     @Execute
235     @Secured({ ROLE, ROLE + VIEW })
236     public HtmlResponse details(final int crudMode, final String id) {
237         verifyCrudMode(crudMode, CrudMode.DETAILS, this::asListHtml);
238         saveToken();
239         return asHtml(path_AdminGroup_AdminGroupDetailsJsp).useForm(EditForm.class, op -> {
240             op.setup(form -> {
241                 groupService.getGroup(id).ifPresent(entity -> {
242                     copyBeanToBean(entity, form, copyOp -> {
243                         copyOp.excludeNull();
244                     });
245                     form.crudMode = crudMode;
246                 }).orElse(() -> {
247                     throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, id), this::asListHtml);
248                 });
249             });
250         });
251     }
252 
253     // -----------------------------------------------------
254     //                                         Actually Crud
255     //                                         -------------
256     /**
257      * Creates a new group.
258      *
259      * @param form the create form
260      * @return HTML response after creation
261      */
262     @Execute
263     @Secured({ ROLE })
264     public HtmlResponse create(final CreateForm form) {
265         verifyCrudMode(form.crudMode, CrudMode.CREATE, this::asListHtml);
266         validate(form, messages -> {}, this::asEditHtml);
267         validateAttributes(form.attributes, v -> throwValidationError(v, this::asEditHtml));
268         verifyToken(this::asEditHtml);
269         getGroup(form).ifPresent(entity -> {
270             try {
271                 groupService.store(entity);
272                 saveInfo(messages -> messages.addSuccessCrudCreateCrudTable(GLOBAL));
273                 logger.info("Created group: {}", entity.getName());
274             } catch (final Exception e) {
275                 logger.warn("Failed to create group: {}", form.name, e);
276                 throwValidationError(messages -> messages.addErrorsCrudFailedToCreateCrudTable(GLOBAL, buildThrowableMessage(e)),
277                         this::asEditHtml);
278             }
279         }).orElse(() -> {
280             throwValidationError(messages -> messages.addErrorsCrudFailedToCreateInstance(GLOBAL), this::asEditHtml);
281         });
282         return redirect(getClass());
283     }
284 
285     /**
286      * Updates an existing group.
287      *
288      * @param form the edit form
289      * @return HTML response after update
290      */
291     @Execute
292     @Secured({ ROLE })
293     public HtmlResponse update(final EditForm form) {
294         verifyCrudMode(form.crudMode, CrudMode.EDIT, this::asListHtml);
295         validate(form, messages -> {}, this::asEditHtml);
296         validateAttributes(form.attributes, v -> throwValidationError(v, this::asEditHtml));
297         verifyToken(this::asEditHtml);
298         getGroup(form).ifPresent(entity -> {
299             try {
300                 groupService.store(entity);
301                 saveInfo(messages -> messages.addSuccessCrudUpdateCrudTable(GLOBAL));
302                 logger.info("Updated group: {}", entity.getName());
303             } catch (final Exception e) {
304                 logger.warn("Failed to update group: {}", form.name, e);
305                 throwValidationError(messages -> messages.addErrorsCrudFailedToUpdateCrudTable(GLOBAL, buildThrowableMessage(e)),
306                         this::asEditHtml);
307             }
308         }).orElse(() -> {
309             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, form.id), this::asEditHtml);
310         });
311         return redirect(getClass());
312     }
313 
314     /**
315      * Deletes a group.
316      *
317      * @param form the edit form
318      * @return HTML response after deletion
319      */
320     @Execute
321     @Secured({ ROLE })
322     public HtmlResponse delete(final EditForm form) {
323         verifyCrudMode(form.crudMode, CrudMode.DETAILS, this::asListHtml);
324         validate(form, messages -> {}, this::asDetailsHtml);
325         verifyToken(this::asDetailsHtml);
326         final String id = form.id;
327         groupService.getGroup(id).ifPresent(entity -> {
328             try {
329                 groupService.delete(entity);
330                 saveInfo(messages -> messages.addSuccessCrudDeleteCrudTable(GLOBAL));
331                 logger.info("Deleted group: {}", entity.getName());
332             } catch (final Exception e) {
333                 logger.warn("Failed to delete group: {}", form.name, e);
334                 throwValidationError(messages -> messages.addErrorsCrudFailedToDeleteCrudTable(GLOBAL, buildThrowableMessage(e)),
335                         this::asDetailsHtml);
336             }
337         }).orElse(() -> {
338             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, id), this::asDetailsHtml);
339         });
340         return redirect(getClass());
341     }
342 
343     // ===================================================================================
344     //                                                                        Assist Logic
345     //                                                                        ============
346     /**
347      * Gets a group entity based on the form.
348      *
349      * @param form the create form
350      * @return optional group entity
351      */
352     private static OptionalEntity<Group> getEntity(final CreateForm form) {
353         switch (form.crudMode) {
354         case CrudMode.CREATE:
355             return OptionalEntity.of(new Group()).map(entity -> {
356                 entity.setId(Base64.getUrlEncoder().encodeToString(form.name.getBytes(Constants.CHARSET_UTF_8)));
357                 return entity;
358             });
359         case CrudMode.EDIT:
360             if (form instanceof EditForm) {
361                 return ComponentUtil.getComponent(GroupService.class).getGroup(((EditForm) form).id);
362             }
363             break;
364         default:
365             break;
366         }
367         return OptionalEntity.empty();
368     }
369 
370     /**
371      * Gets a group entity from the form with attributes.
372      *
373      * @param form the create form
374      * @return optional group entity
375      */
376     public static OptionalEntity<Group> getGroup(final CreateForm form) {
377         return getEntity(form).map(entity -> {
378             copyMapToBean(form.attributes, entity, op -> op.exclude(Constants.COMMON_CONVERSION_RULE));
379             copyBeanToBean(form, entity, op -> op.exclude(Constants.COMMON_CONVERSION_RULE));
380             return entity;
381         });
382     }
383 
384     // ===================================================================================
385     //                                                                        Small Helper
386     //                                                                        ============
387 
388     /**
389      * Validates group attributes using LDAP manager.
390      *
391      * @param attributes the attributes to validate
392      * @param throwError the error handler
393      */
394     public static void validateAttributes(final Map<String, String> attributes, final Consumer<VaMessenger<FessMessages>> throwError) {
395         ComponentUtil.getLdapManager()
396                 .validateGroupAttributes(Long.class, attributes,
397                         s -> throwError.accept(messages -> messages.addErrorsPropertyTypeLong("attributes." + s, "attributes." + s)));
398     }
399 
400     // ===================================================================================
401     //                                                                              JSP
402     //                                                                           =========
403 
404     /**
405      * Returns HTML response for the list page.
406      *
407      * @return HTML response for the list page
408      */
409     private HtmlResponse asListHtml() {
410         return asHtml(path_AdminGroup_AdminGroupJsp).renderWith(data -> {
411             RenderDataUtil.register(data, "groupItems", groupService.getGroupList(groupPager)); // page navi
412         }).useForm(SearchForm.class, setup -> {
413             setup.setup(form -> {
414                 copyBeanToBean(groupPager, form, op -> op.include("id"));
415             });
416         });
417     }
418 
419     /**
420      * Returns HTML response for the edit page.
421      *
422      * @return HTML response for the edit page
423      */
424     private HtmlResponse asEditHtml() {
425         return asHtml(path_AdminGroup_AdminGroupEditJsp);
426     }
427 
428     /**
429      * Returns HTML response for the details page.
430      *
431      * @return HTML response for the details page
432      */
433     private HtmlResponse asDetailsHtml() {
434         return asHtml(path_AdminGroup_AdminGroupDetailsJsp);
435     }
436 }