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.scheduler;
17  
18  import java.text.MessageFormat;
19  import java.util.Base64;
20  
21  import org.apache.logging.log4j.LogManager;
22  import org.apache.logging.log4j.Logger;
23  import org.codelibs.fess.Constants;
24  import org.codelibs.fess.annotation.Secured;
25  import org.codelibs.fess.app.pager.SchedulerPager;
26  import org.codelibs.fess.app.service.ScheduledJobService;
27  import org.codelibs.fess.app.web.CrudMode;
28  import org.codelibs.fess.app.web.base.FessAdminAction;
29  import org.codelibs.fess.helper.ProcessHelper;
30  import org.codelibs.fess.helper.SystemHelper;
31  import org.codelibs.fess.opensearch.config.exentity.ScheduledJob;
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.util.LaRequestUtil;
41  
42  import jakarta.annotation.Resource;
43  
44  /**
45   * Admin action for Scheduler management.
46   *
47   */
48  public class AdminSchedulerAction extends FessAdminAction {
49  
50      /**
51       * Default constructor.
52       */
53      public AdminSchedulerAction() {
54          super();
55      }
56  
57      /** Role name for admin scheduler operations */
58      public static final String ROLE = "admin-scheduler";
59  
60      private static final Logger logger = LogManager.getLogger(AdminSchedulerAction.class);
61  
62      // ===================================================================================
63      //                                                                           Attribute
64      //                                                                           =========
65      /** Service for managing scheduled jobs */
66      @Resource
67      private ScheduledJobService scheduledJobService;
68      /** Pager for paginating scheduled job results */
69      @Resource
70      private SchedulerPager schedulerPager;
71      /** Helper for processing scheduled jobs. */
72      @Resource
73      protected ProcessHelper processHelper;
74  
75      // ===================================================================================
76      //                                                                               Hook
77      //                                                                              ======
78      @Override
79      protected void setupHtmlData(final ActionRuntime runtime) {
80          super.setupHtmlData(runtime);
81          runtime.registerData("helpLink", systemHelper.getHelpLink(fessConfig.getOnlineHelpNameScheduler()));
82      }
83  
84      @Override
85      protected String getActionRole() {
86          return ROLE;
87      }
88  
89      // ===================================================================================
90      //                                                                      Search Execute
91      //                                                                      ==============
92      /**
93       * Displays the scheduler management index page.
94       *
95       * @param form the search form for filtering
96       * @return HTML response for the scheduler list page
97       */
98      @Execute
99      @Secured({ ROLE, ROLE + VIEW })
100     public HtmlResponse index(final SearchForm form) {
101         return asListHtml();
102     }
103 
104     /**
105      * Displays a paginated list of scheduled jobs.
106      *
107      * @param pageNumber the page number to display (optional)
108      * @param form the search form containing filter criteria
109      * @return HTML response with the scheduled job list
110      */
111     @Execute
112     @Secured({ ROLE, ROLE + VIEW })
113     public HtmlResponse list(final OptionalThing<Integer> pageNumber, final SearchForm form) {
114         pageNumber.ifPresent(num -> {
115             schedulerPager.setCurrentPageNumber(pageNumber.get());
116         }).orElse(() -> {
117             schedulerPager.setCurrentPageNumber(0);
118         });
119         return asHtml(path_AdminScheduler_AdminSchedulerJsp).renderWith(data -> {
120             searchPaging(data, form);
121         });
122     }
123 
124     /**
125      * Searches for scheduled jobs based on the provided search criteria.
126      *
127      * @param form the search form containing search criteria
128      * @return HTML response with filtered scheduled job results
129      */
130     @Execute
131     @Secured({ ROLE, ROLE + VIEW })
132     public HtmlResponse search(final SearchForm form) {
133         copyBeanToBean(form, schedulerPager, op -> op.exclude(Constants.PAGER_CONVERSION_RULE));
134         return asHtml(path_AdminScheduler_AdminSchedulerJsp).renderWith(data -> {
135             searchPaging(data, form);
136         });
137     }
138 
139     /**
140      * Resets the search criteria and displays all scheduled jobs.
141      *
142      * @param form the search form to reset
143      * @return HTML response with the reset scheduled job list
144      */
145     @Execute
146     @Secured({ ROLE, ROLE + VIEW })
147     public HtmlResponse reset(final SearchForm form) {
148         schedulerPager.clear();
149         return asHtml(path_AdminScheduler_AdminSchedulerJsp).renderWith(data -> {
150             searchPaging(data, form);
151         });
152     }
153 
154     /**
155      * Sets up search paging data for rendering the scheduled job list.
156      *
157      * @param data the render data to populate
158      * @param form the search form containing current search criteria
159      */
160     protected void searchPaging(final RenderData data, final SearchForm form) {
161         RenderDataUtil.register(data, "scheduledJobItems", scheduledJobService.getScheduledJobList(schedulerPager)); // page navi
162 
163         // restore from pager
164         copyBeanToBean(schedulerPager, form, op -> op.include("id"));
165     }
166 
167     // ===================================================================================
168     //                                                                        Edit Execute
169     //                                                                        ============
170     // -----------------------------------------------------
171     //                                            Entry Page
172     //                                            ----------
173 
174     /**
175      * Creates a new scheduled job from a crawler configuration.
176      *
177      * @param type the crawler type (web, file, or data)
178      * @param id the crawler configuration ID
179      * @param name the name for the new job (base64 encoded)
180      * @return HTML response for the job creation form
181      */
182     @Execute
183     @Secured({ ROLE })
184     public HtmlResponse createnewjob(final String type, final String id, final String name) {
185         saveToken();
186         return asHtml(path_AdminScheduler_AdminSchedulerEditJsp).useForm(CreateForm.class, op -> {
187             op.setup(scheduledJobForm -> {
188                 scheduledJobForm.initialize();
189                 scheduledJobForm.crudMode = CrudMode.CREATE;
190                 scheduledJobForm.jobLogging = Constants.ON;
191                 scheduledJobForm.crawler = Constants.ON;
192                 scheduledJobForm.available = Constants.ON;
193                 scheduledJobForm.cronExpression = null;
194                 final String decodedName = new String(Base64.getUrlDecoder().decode(name), Constants.CHARSET_UTF_8);
195                 scheduledJobForm.name = MessageFormat.format(fessConfig.getJobTemplateTitle(type), decodedName);
196                 final String[] ids = { "", "", "" };
197                 if (Constants.WEB_CRAWLER_TYPE.equals(type)) {
198                     ids[0] = "\"" + id + "\"";
199                 } else if (Constants.FILE_CRAWLER_TYPE.equals(type)) {
200                     ids[1] = "\"" + id + "\"";
201                 } else if (Constants.DATA_CRAWLER_TYPE.equals(type)) {
202                     ids[2] = "\"" + id + "\"";
203                 }
204                 scheduledJobForm.scriptData =
205                         MessageFormat.format(fessConfig.getJobTemplateScript(), ids[0], ids[1], ids[2], id.replace('-', '_'));
206             });
207         });
208     }
209 
210     /**
211      * Displays the form for creating a new scheduled job.
212      *
213      * @return HTML response for the job creation form
214      */
215     @Execute
216     @Secured({ ROLE })
217     public HtmlResponse createnew() {
218         saveToken();
219         return asHtml(path_AdminScheduler_AdminSchedulerEditJsp).useForm(CreateForm.class, op -> {
220             op.setup(form -> {
221                 form.initialize();
222                 form.crudMode = CrudMode.CREATE;
223             });
224         });
225     }
226 
227     /**
228      * Displays the form for editing an existing scheduled job.
229      *
230      * @param form the edit form containing the ID of the job to edit
231      * @return HTML response for the job edit form
232      */
233     @Execute
234     @Secured({ ROLE })
235     public HtmlResponse edit(final EditForm form) {
236         validate(form, messages -> {}, this::asListHtml);
237         final String id = form.id;
238         scheduledJobService.getScheduledJob(id).ifPresent(entity -> {
239             loadScheduledJob(form, entity);
240         }).orElse(() -> {
241             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, id), this::asListHtml);
242         });
243         saveToken();
244         if (form.crudMode.intValue() == CrudMode.EDIT) {
245             // back
246             form.crudMode = CrudMode.DETAILS;
247             return asDetailsHtml(id);
248         }
249         form.crudMode = CrudMode.EDIT;
250         return asEditHtml();
251     }
252 
253     // -----------------------------------------------------
254     //                                               Details
255     //                                               -------
256     /**
257      * Displays the details of a scheduled job.
258      *
259      * @param crudMode the CRUD mode for the operation
260      * @param id the ID of the scheduled job to display
261      * @return HTML response for the job details page
262      */
263     @Execute
264     @Secured({ ROLE, ROLE + VIEW })
265     public HtmlResponse details(final int crudMode, final String id) {
266         verifyCrudMode(crudMode, CrudMode.DETAILS, this::asListHtml);
267         saveToken();
268         return asHtml(path_AdminScheduler_AdminSchedulerDetailsJsp).renderWith(data -> {
269             RenderDataUtil.register(data, "systemJobId", fessConfig.isSystemJobId(id));
270         }).useForm(EditForm.class, op -> {
271             op.setup(form -> {
272                 scheduledJobService.getScheduledJob(id).ifPresent(entity -> {
273                     loadScheduledJob(form, entity);
274                     form.crudMode = crudMode;
275                     LaRequestUtil.getOptionalRequest().ifPresent(request -> {
276                         request.setAttribute("running", entity.isRunning());
277                         request.setAttribute("enabled", entity.isEnabled());
278                     });
279                 }).orElse(() -> {
280                     throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, id), this::asListHtml);
281                 });
282             });
283         });
284     }
285 
286     // -----------------------------------------------------
287     //                                         Actually Crud
288     //                                         -------------
289     /**
290      * Creates a new scheduled job.
291      *
292      * @param form the create form containing the new job data
293      * @return HTML response redirecting to the list page after creation
294      */
295     @Execute
296     @Secured({ ROLE })
297     public HtmlResponse create(final CreateForm form) {
298         verifyCrudMode(form.crudMode, CrudMode.CREATE, this::asListHtml);
299         validate(form, messages -> {}, this::asEditHtml);
300         verifyToken(this::asEditHtml);
301         getScheduledJob(form).ifPresent(entity -> {
302             try {
303                 scheduledJobService.store(entity);
304                 saveInfo(messages -> messages.addSuccessCrudCreateCrudTable(GLOBAL));
305             } catch (final Exception e) {
306                 logger.warn("Failed to process a request.", e);
307                 throwValidationError(messages -> messages.addErrorsCrudFailedToCreateCrudTable(GLOBAL, buildThrowableMessage(e)),
308                         this::asEditHtml);
309             }
310         }).orElse(() -> {
311             throwValidationError(messages -> messages.addErrorsCrudFailedToCreateInstance(GLOBAL), this::asEditHtml);
312         });
313         return redirect(getClass());
314     }
315 
316     /**
317      * Updates an existing scheduled job.
318      *
319      * @param form the edit form containing the updated job data
320      * @return HTML response redirecting to the list page after update
321      */
322     @Execute
323     @Secured({ ROLE })
324     public HtmlResponse update(final EditForm form) {
325         verifyCrudMode(form.crudMode, CrudMode.EDIT, this::asListHtml);
326         validate(form, messages -> {}, this::asEditHtml);
327         verifyToken(this::asEditHtml);
328         getScheduledJob(form).ifPresent(entity -> {
329             try {
330                 scheduledJobService.store(entity);
331                 saveInfo(messages -> messages.addSuccessCrudUpdateCrudTable(GLOBAL));
332             } catch (final Exception e) {
333                 logger.warn("Failed to process a request.", e);
334                 throwValidationError(messages -> messages.addErrorsCrudFailedToUpdateCrudTable(GLOBAL, buildThrowableMessage(e)),
335                         this::asEditHtml);
336             }
337         }).orElse(() -> {
338             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, form.id), this::asEditHtml);
339         });
340         return redirect(getClass());
341     }
342 
343     /**
344      * Deletes a scheduled job.
345      *
346      * @param form the edit form containing the ID of the job to delete
347      * @return HTML response redirecting to the list page after deletion
348      */
349     @Execute
350     @Secured({ ROLE })
351     public HtmlResponse delete(final EditForm form) {
352         verifyCrudMode(form.crudMode, CrudMode.DETAILS, this::asListHtml);
353         final String id = form.id;
354         validate(form, messages -> {}, () -> asDetailsHtml(id));
355         verifyToken(() -> asDetailsHtml(id));
356         scheduledJobService.getScheduledJob(id).ifPresent(entity -> {
357             try {
358                 scheduledJobService.delete(entity);
359                 saveInfo(messages -> messages.addSuccessCrudDeleteCrudTable(GLOBAL));
360             } catch (final Exception e) {
361                 logger.warn("Failed to process a request.", e);
362                 throwValidationError(messages -> messages.addErrorsCrudFailedToDeleteCrudTable(GLOBAL, buildThrowableMessage(e)),
363                         this::asEditHtml);
364             }
365         }).orElse(() -> {
366             throwValidationError(messages -> messages.addErrorsCrudCouldNotFindCrudTable(GLOBAL, id), () -> asDetailsHtml(id));
367         });
368         return redirect(getClass());
369     }
370 
371     /**
372      * Starts a scheduled job.
373      *
374      * @param form the edit form containing the ID of the job to start
375      * @return HTML response redirecting to the list page after starting
376      */
377     @Execute
378     @Secured({ ROLE })
379     public HtmlResponse start(final EditForm form) {
380         verifyCrudMode(form.crudMode, CrudMode.DETAILS, this::asListHtml);
381         final String id = form.id;
382         validate(form, messages -> {}, () -> asDetailsHtml(id));
383         verifyToken(() -> asDetailsHtml(id));
384         scheduledJobService.getScheduledJob(id).ifPresent(entity -> {
385             if (!entity.isEnabled() || entity.isRunning()) {
386                 throwValidationError(messages -> {
387                     messages.addErrorsFailedToStartJob(GLOBAL, entity.getName());
388                 }, () -> asDetailsHtml(id));
389             }
390             try {
391                 entity.start();
392                 saveInfo(messages -> messages.addSuccessJobStarted(GLOBAL, entity.getName()));
393             } catch (final Exception e) {
394                 logger.warn("Failed to process a request.", e);
395                 throwValidationError(messages -> {
396                     messages.addErrorsFailedToStartJob(GLOBAL, entity.getName());
397                 }, () -> asDetailsHtml(id));
398             }
399         }).orElse(() -> {
400             throwValidationError(messages -> {
401                 messages.addErrorsFailedToStartJob(GLOBAL, id);
402             }, () -> asDetailsHtml(id));
403         });
404         return redirect(getClass());
405     }
406 
407     /**
408      * Stops a running scheduled job.
409      *
410      * @param form the edit form containing the ID of the job to stop
411      * @return HTML response redirecting to the list page after stopping
412      */
413     @Execute
414     @Secured({ ROLE })
415     public HtmlResponse stop(final EditForm form) {
416         verifyCrudMode(form.crudMode, CrudMode.DETAILS, this::asListHtml);
417         final String id = form.id;
418         validate(form, messages -> {}, () -> asDetailsHtml(id));
419         verifyToken(() -> asDetailsHtml(id));
420         scheduledJobService.getScheduledJob(id).ifPresent(entity -> {
421             try {
422                 entity.stop();
423                 saveInfo(messages -> messages.addSuccessJobStopped(GLOBAL, entity.getName()));
424             } catch (final Exception e) {
425                 logger.warn("Failed to process a request.", e);
426                 throwValidationError(messages -> {
427                     messages.addErrorsFailedToStopJob(GLOBAL, entity.getName());
428                 }, () -> asDetailsHtml(id));
429             }
430         }).orElse(() -> {
431             throwValidationError(messages -> {
432                 messages.addErrorsFailedToStartJob(GLOBAL, id);
433             }, () -> asDetailsHtml(id));
434         });
435         return redirect(getClass());
436     }
437 
438     // ===================================================================================
439     //                                                                        Assist Logic
440     //                                                                        ============
441     /**
442      * Loads scheduled job data into the edit form.
443      *
444      * @param form the edit form to populate
445      * @param entity the scheduled job entity to load from
446      */
447     protected void loadScheduledJob(final EditForm form, final ScheduledJob entity) {
448         copyBeanToBean(entity, form, op -> op.exclude("crudMode").excludeNull());
449         form.jobLogging = entity.isLoggingEnabled() ? Constants.ON : null;
450         form.crawler = entity.isCrawlerJob() ? Constants.ON : null;
451         form.available = entity.isEnabled() ? Constants.ON : null;
452     }
453 
454     /**
455      * Creates a ScheduledJob entity from form data with user and timestamp information.
456      *
457      * @param form the form containing the scheduled job data
458      * @param username the username of the user performing the operation
459      * @param currentTime the current timestamp
460      * @return optional entity containing the scheduled job data, or empty if creation fails
461      */
462     private static OptionalEntity<ScheduledJob> getEntity(final CreateForm form, final String username, final long currentTime) {
463         switch (form.crudMode) {
464         case CrudMode.CREATE:
465             return OptionalEntity.of(new ScheduledJob()).map(entity -> {
466                 entity.setCreatedBy(username);
467                 entity.setCreatedTime(currentTime);
468                 return entity;
469             });
470         case CrudMode.EDIT:
471             if (form instanceof EditForm) {
472                 return ComponentUtil.getComponent(ScheduledJobService.class).getScheduledJob(((EditForm) form).id);
473             }
474             break;
475         default:
476             break;
477         }
478         return OptionalEntity.empty();
479     }
480 
481     /**
482      * Creates a ScheduledJob entity from the provided form data.
483      *
484      * @param form the form containing the scheduled job data
485      * @return optional entity containing the scheduled job data, or empty if creation fails
486      */
487     public static OptionalEntity<ScheduledJob> getScheduledJob(final CreateForm form) {
488         final SystemHelper systemHelper = ComponentUtil.getSystemHelper();
489         final String username = systemHelper.getUsername();
490         final long currentTime = systemHelper.getCurrentTimeAsLong();
491         return getEntity(form, username, currentTime).map(entity -> {
492             entity.setUpdatedBy(username);
493             entity.setUpdatedTime(currentTime);
494             copyBeanToBean(form, entity, op -> op.exclude(Constants.COMMON_CONVERSION_RULE));
495             entity.setJobLogging(isCheckboxEnabled(form.jobLogging) ? Constants.T : Constants.F);
496             entity.setCrawler(isCheckboxEnabled(form.crawler) ? Constants.T : Constants.F);
497             entity.setAvailable(isCheckboxEnabled(form.available) ? Constants.T : Constants.F);
498             return entity;
499         });
500     }
501 
502     // ===================================================================================
503     //                                                                        Small Helper
504     //                                                                        ============
505     //                                                                              JSP
506     //                                                                           =========
507 
508     private HtmlResponse asListHtml() {
509         return asHtml(path_AdminScheduler_AdminSchedulerJsp).renderWith(data -> {
510             RenderDataUtil.register(data, "scheduledJobItems", scheduledJobService.getScheduledJobList(schedulerPager)); // page navi
511         }).useForm(SearchForm.class, setup -> {
512             setup.setup(form -> {
513                 copyBeanToBean(schedulerPager, form, op -> op.include("id"));
514             });
515         });
516     }
517 
518     private HtmlResponse asEditHtml() {
519         return asHtml(path_AdminScheduler_AdminSchedulerEditJsp);
520     }
521 
522     private HtmlResponse asDetailsHtml(final String id) {
523         return asHtml(path_AdminScheduler_AdminSchedulerDetailsJsp).renderWith(data -> {
524             RenderDataUtil.register(data, "systemJobId", fessConfig.isSystemJobId(id));
525         });
526     }
527 }