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.accesstoken;
17  
18  import static org.codelibs.core.stream.StreamUtil.stream;
19  
20  import java.util.stream.Collectors;
21  
22  import org.apache.logging.log4j.LogManager;
23  import org.apache.logging.log4j.Logger;
24  import org.codelibs.core.beans.util.BeanUtil;
25  import org.codelibs.core.lang.StringUtil;
26  import org.codelibs.fess.Constants;
27  import org.codelibs.fess.annotation.Secured;
28  import org.codelibs.fess.app.pager.AccessTokenPager;
29  import org.codelibs.fess.app.service.AccessTokenService;
30  import org.codelibs.fess.app.web.CrudMode;
31  import org.codelibs.fess.app.web.base.FessAdminAction;
32  import org.codelibs.fess.helper.PermissionHelper;
33  import org.codelibs.fess.helper.SystemHelper;
34  import org.codelibs.fess.opensearch.config.exentity.AccessToken;
35  import org.codelibs.fess.util.ComponentUtil;
36  import org.codelibs.fess.util.RenderDataUtil;
37  import org.dbflute.optional.OptionalEntity;
38  import org.dbflute.optional.OptionalThing;
39  import org.lastaflute.web.Execute;
40  import org.lastaflute.web.response.HtmlResponse;
41  import org.lastaflute.web.response.render.RenderData;
42  import org.lastaflute.web.ruts.process.ActionRuntime;
43  
44  import jakarta.annotation.Resource;
45  
46  /**
47   * Admin action for Access Token management.
48   * This class provides CRUD operations for access tokens.
49   *
50   */
51  public class AdminAccesstokenAction extends FessAdminAction {
52  
53      /**
54       * Default constructor.
55       */
56      public AdminAccesstokenAction() {
57          super();
58      }
59  
60      private static final Logger logger = LogManager.getLogger(AdminAccesstokenAction.class);
61  
62      /**
63       * The role for this action.
64       */
65      public static final String ROLE = "admin-accesstoken";
66  
67      /**
68       * The token parameter.
69       */
70      public static final String TOKEN = "token";
71  
72      /**
73       * The expires parameter.
74       */
75      public static final String EXPIRES = "expires";
76  
77      /**
78       * The expired time parameter.
79       */
80      public static final String EXPIRED_TIME = "expiredTime";
81  
82      // ===================================================================================
83      //                                                                           Attribute
84      //                                                                           =========
85      @Resource
86      private AccessTokenService accessTokenService;
87      @Resource
88      private AccessTokenPager accessTokenPager;
89  
90      // ===================================================================================
91      //                                                                               Hook
92      //                                                                              ======
93      @Override
94      protected void setupHtmlData(final ActionRuntime runtime) {
95          super.setupHtmlData(runtime);
96          runtime.registerData("helpLink", systemHelper.getHelpLink(fessConfig.getOnlineHelpNameAccesstoken()));
97      }
98  
99      @Override
100     protected String getActionRole() {
101         return ROLE;
102     }
103 
104     // ===================================================================================
105     //                                                                      Search Execute
106     //                                                                      ==============
107     /**
108      * Show the index page.
109      * @return The HTML response.
110      */
111     @Execute
112     @Secured({ ROLE, ROLE + VIEW })
113     public HtmlResponse index() {
114         return asListHtml();
115     }
116 
117     /**
118      * Show the list page.
119      * @param pageNumber The page number.
120      * @param form The search form.
121      * @return The HTML response.
122      */
123     @Execute
124     @Secured({ ROLE, ROLE + VIEW })
125     public HtmlResponse list(final OptionalThing<Integer> pageNumber, final SearchForm form) {
126         pageNumber.ifPresent(num -> {
127             accessTokenPager.setCurrentPageNumber(pageNumber.get());
128         }).orElse(() -> {
129             accessTokenPager.setCurrentPageNumber(0);
130         });
131         return asHtml(path_AdminAccesstoken_AdminAccesstokenJsp).renderWith(data -> {
132             searchPaging(data, form);
133         });
134     }
135 
136     /**
137      * Search the access tokens.
138      * @param form The search form.
139      * @return The HTML response.
140      */
141     @Execute
142     @Secured({ ROLE, ROLE + VIEW })
143     public HtmlResponse search(final SearchForm form) {
144         copyBeanToBean(form, accessTokenPager, op -> op.exclude(Constants.PAGER_CONVERSION_RULE));
145         return asHtml(path_AdminAccesstoken_AdminAccesstokenJsp).renderWith(data -> {
146             searchPaging(data, form);
147         });
148     }
149 
150     /**
151      * Reset the search form.
152      * @param form The search form.
153      * @return The HTML response.
154      */
155     @Execute
156     @Secured({ ROLE, ROLE + VIEW })
157     public HtmlResponse reset(final SearchForm form) {
158         accessTokenPager.clear();
159         return asHtml(path_AdminAccesstoken_AdminAccesstokenJsp).renderWith(data -> {
160             searchPaging(data, form);
161         });
162     }
163 
164     /**
165      * Search with paging.
166      * @param data The render data.
167      * @param form The search form.
168      */
169     protected void searchPaging(final RenderData data, final SearchForm form) {
170         RenderDataUtil.register(data, "accessTokenItems", accessTokenService.getAccessTokenList(accessTokenPager)); // page navi
171 
172         // restore from pager
173         copyBeanToBean(accessTokenPager, form, op -> op.include("id"));
174     }
175 
176     // ===================================================================================
177     //                                                                        Edit Execute
178     //                                                                        ============
179     // -----------------------------------------------------
180     //                                            Entry Page
181     //                                            ----------
182     /**
183      * Show the create new page.
184      * @return The HTML response.
185      */
186     @Execute
187     @Secured({ ROLE })
188     public HtmlResponse createnew() {
189         saveToken();
190         return asEditHtml().useForm(CreateForm.class, op -> {
191             op.setup(form -> {
192                 form.initialize();
193                 form.crudMode = CrudMode.CREATE;
194             });
195         });
196     }
197 
198     // -----------------------------------------------------
199     //                                               Details
200     //                                               -------
201     /**
202      * Show the details page.
203      * @param crudMode The CRUD mode.
204      * @param id The ID.
205      * @return The HTML response.
206      */
207     @Execute
208     @Secured({ ROLE, ROLE + VIEW })
209     public HtmlResponse details(final int crudMode, final String id) {
210         verifyCrudMode(crudMode, CrudMode.DETAILS, this::asListHtml);
211         saveToken();
212         return asDetailsHtml().useForm(EditForm.class, op -> {
213             op.setup(form -> {
214                 accessTokenService.getAccessToken(id).ifPresent(entity -> {
215                     copyBeanToBean(entity, form,
216                             copyOp -> copyOp.exclude(Constants.PERMISSIONS, EXPIRED_TIME)
217                                     .excludeNull()
218                                     .dateConverter(Constants.DEFAULT_DATETIME_FORMAT, EXPIRES));
219                     final PermissionHelper permissionHelper = ComponentUtil.getPermissionHelper();
220                     form.permissions = stream(entity.getPermissions()).get(stream -> stream.map(permissionHelper::decode)
221                             .filter(StringUtil::isNotBlank)
222                             .distinct()
223                             .collect(Collectors.joining("\n")));
224                     form.crudMode = crudMode;
225                 }).orElse(() -> {
226                     throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, id), this::asListHtml);
227                 });
228             });
229         });
230     }
231 
232     /**
233      * Show the edit page.
234      * @param form The edit form.
235      * @return The HTML response.
236      */
237     @Execute
238     @Secured({ ROLE })
239     public HtmlResponse edit(final EditForm form) {
240         validate(form, messages -> {}, this::asListHtml);
241         final String id = form.id;
242         accessTokenService.getAccessToken(id).ifPresent(entity -> {
243             copyBeanToBean(entity, form,
244                     op -> op.exclude(Constants.PERMISSIONS, EXPIRED_TIME).dateConverter(Constants.DEFAULT_DATETIME_FORMAT, EXPIRES));
245             final PermissionHelper permissionHelper = ComponentUtil.getPermissionHelper();
246             form.permissions = stream(entity.getPermissions()).get(stream -> stream.map(permissionHelper::decode)
247                     .filter(StringUtil::isNotBlank)
248                     .distinct()
249                     .collect(Collectors.joining("\n")));
250         }).orElse(() -> {
251             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, id), this::asListHtml);
252         });
253         saveToken();
254         if (form.crudMode.intValue() == CrudMode.EDIT) {
255             // back
256             form.crudMode = CrudMode.DETAILS;
257             return asDetailsHtml();
258         }
259         form.crudMode = CrudMode.EDIT;
260         return asEditHtml();
261     }
262 
263     // -----------------------------------------------------
264     //                                         Actually Crud
265     //                                         -------------
266     /**
267      * Create an access token.
268      * @param form The create form.
269      * @return The HTML response.
270      */
271     @Execute
272     @Secured({ ROLE })
273     public HtmlResponse create(final CreateForm form) {
274         verifyCrudMode(form.crudMode, CrudMode.CREATE, this::asListHtml);
275         validate(form, messages -> {}, this::asEditHtml);
276         verifyToken(this::asEditHtml);
277         getAccessToken(form).ifPresent(entity -> {
278             entity.setToken(accessTokenHelper.generateAccessToken());
279             try {
280                 accessTokenService.store(entity);
281                 saveInfo(messages -> messages.addSuccessCrudCreateCrudTable(GLOBAL));
282             } catch (final Exception e) {
283                 logger.warn("Failed to process a request.", e);
284                 throwValidationError(messages -> messages.addErrorsCrudFailedToCreateCrudTable(GLOBAL, buildThrowableMessage(e)),
285                         this::asEditHtml);
286             }
287         }).orElse(() -> {
288             throwValidationError(messages -> messages.addErrorsCrudFailedToCreateInstance(GLOBAL), this::asEditHtml);
289         });
290         return redirect(getClass());
291     }
292 
293     /**
294      * Update an access token.
295      * @param form The edit form.
296      * @return The HTML response.
297      */
298     @Execute
299     @Secured({ ROLE })
300     public HtmlResponse update(final EditForm form) {
301         verifyCrudMode(form.crudMode, CrudMode.EDIT, this::asListHtml);
302         validate(form, messages -> {}, this::asEditHtml);
303         verifyToken(this::asEditHtml);
304         getAccessToken(form).ifPresent(entity -> {
305             try {
306                 accessTokenService.store(entity);
307                 saveInfo(messages -> messages.addSuccessCrudUpdateCrudTable(GLOBAL));
308             } catch (final Exception e) {
309                 logger.warn("Failed to process a request.", e);
310                 throwValidationError(messages -> messages.addErrorsCrudFailedToUpdateCrudTable(GLOBAL, buildThrowableMessage(e)),
311                         this::asEditHtml);
312             }
313         }).orElse(() -> {
314             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, form.id), this::asEditHtml);
315         });
316         return redirect(getClass());
317     }
318 
319     /**
320      * Delete an access token.
321      * @param form The edit form.
322      * @return The HTML response.
323      */
324     @Execute
325     @Secured({ ROLE })
326     public HtmlResponse delete(final EditForm form) {
327         verifyCrudMode(form.crudMode, CrudMode.DETAILS, this::asListHtml);
328         validate(form, messages -> {}, this::asDetailsHtml);
329         verifyToken(this::asDetailsHtml);
330         final String id = form.id;
331         accessTokenService.getAccessToken(id).ifPresent(entity -> {
332             try {
333                 accessTokenService.delete(entity);
334                 saveInfo(messages -> messages.addSuccessCrudDeleteCrudTable(GLOBAL));
335             } catch (final Exception e) {
336                 logger.warn("Failed to process a request.", e);
337                 throwValidationError(messages -> messages.addErrorsCrudFailedToDeleteCrudTable(GLOBAL, buildThrowableMessage(e)),
338                         this::asEditHtml);
339             }
340         }).orElse(() -> {
341             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, id), this::asDetailsHtml);
342         });
343         return redirect(getClass());
344     }
345 
346     // ===================================================================================
347     //                                                                        Assist Logic
348     //                                                                        ============
349 
350     private static OptionalEntity<AccessToken> getEntity(final CreateForm form, final String username, final long currentTime) {
351         switch (form.crudMode) {
352         case CrudMode.CREATE:
353             return OptionalEntity.of(new AccessToken()).map(entity -> {
354                 entity.setCreatedBy(username);
355                 entity.setCreatedTime(currentTime);
356                 return entity;
357             });
358         case CrudMode.EDIT:
359             if (form instanceof EditForm) {
360                 return ComponentUtil.getComponent(AccessTokenService.class).getAccessToken(((EditForm) form).id);
361             }
362             break;
363         default:
364             break;
365         }
366         return OptionalEntity.empty();
367     }
368 
369     /**
370      * Get the access token.
371      * @param form The create form.
372      * @return The access token.
373      */
374     public static OptionalEntity<AccessToken> getAccessToken(final CreateForm form) {
375         final SystemHelper systemHelper = ComponentUtil.getSystemHelper();
376         final String username = systemHelper.getUsername();
377         final long currentTime = systemHelper.getCurrentTimeAsLong();
378         return getEntity(form, username, currentTime).map(entity -> {
379             entity.setUpdatedBy(username);
380             entity.setUpdatedTime(currentTime);
381             BeanUtil.copyBeanToBean(form, entity,
382                     op -> op.exclude(Constants.COMMON_CONVERSION_RULE)
383                             .exclude(TOKEN, Constants.PERMISSIONS, EXPIRED_TIME)
384                             .dateConverter(Constants.DEFAULT_DATETIME_FORMAT, EXPIRES));
385             entity.setPermissions(encodePermissions(form.permissions));
386             return entity;
387         });
388     }
389 
390     // ===================================================================================
391     //                                                                              JSP
392     //                                                                           =========
393 
394     private HtmlResponse asListHtml() {
395         return asHtml(path_AdminAccesstoken_AdminAccesstokenJsp).renderWith(data -> {
396             RenderDataUtil.register(data, "accessTokenItems", accessTokenService.getAccessTokenList(accessTokenPager));
397         }).useForm(SearchForm.class, setup -> {
398             setup.setup(form -> {
399                 copyBeanToBean(accessTokenPager, form, op -> op.include("id"));
400             });
401         });
402     }
403 
404     private HtmlResponse asEditHtml() {
405         return asHtml(path_AdminAccesstoken_AdminAccesstokenEditJsp);
406     }
407 
408     private HtmlResponse asDetailsHtml() {
409         return asHtml(path_AdminAccesstoken_AdminAccesstokenDetailsJsp);
410     }
411 
412 }