001/* 002 * Copyright 2016 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.plugins.core.schedule; 017 018import java.time.ZoneOffset; 019import java.util.ArrayList; 020import java.util.Collections; 021import java.util.Date; 022import java.util.HashMap; 023import java.util.LinkedHashMap; 024import java.util.List; 025import java.util.Map; 026import java.util.Objects; 027import java.util.Optional; 028import java.util.Properties; 029import java.util.Set; 030import java.util.TimeZone; 031import java.util.stream.Collectors; 032 033import org.apache.avalon.framework.activity.Disposable; 034import org.apache.avalon.framework.activity.Initializable; 035import org.apache.avalon.framework.component.Component; 036import org.apache.avalon.framework.context.Context; 037import org.apache.avalon.framework.context.ContextException; 038import org.apache.avalon.framework.context.Contextualizable; 039import org.apache.avalon.framework.service.ServiceException; 040import org.apache.avalon.framework.service.ServiceManager; 041import org.apache.avalon.framework.service.Serviceable; 042import org.apache.cocoon.components.ContextHelper; 043import org.apache.cocoon.environment.Request; 044import org.apache.commons.lang3.StringUtils; 045import org.apache.excalibur.source.SourceResolver; 046import org.quartz.CronScheduleBuilder; 047import org.quartz.CronTrigger; 048import org.quartz.JobBuilder; 049import org.quartz.JobDataMap; 050import org.quartz.JobDetail; 051import org.quartz.JobExecutionContext; 052import org.quartz.JobKey; 053import org.quartz.SchedulerException; 054import org.quartz.Trigger; 055import org.quartz.Trigger.TriggerState; 056import org.quartz.TriggerBuilder; 057import org.quartz.TriggerKey; 058import org.quartz.impl.StdSchedulerFactory; 059import org.quartz.impl.matchers.GroupMatcher; 060import org.quartz.utils.PoolingConnectionProvider; 061 062import org.ametys.core.datasource.ConnectionHelper; 063import org.ametys.core.datasource.SQLDataSourceManager; 064import org.ametys.core.datasource.dbtype.SQLDatabaseType; 065import org.ametys.core.datasource.dbtype.SQLDatabaseTypeExtensionPoint; 066import org.ametys.core.right.RightManager; 067import org.ametys.core.schedule.AmetysJob; 068import org.ametys.core.schedule.Runnable; 069import org.ametys.core.schedule.Runnable.FireProcess; 070import org.ametys.core.schedule.RunnableExtensionPoint; 071import org.ametys.core.schedule.Schedulable; 072import org.ametys.core.schedule.SchedulableExtensionPoint; 073import org.ametys.core.script.SQLScriptHelper; 074import org.ametys.core.ui.Callable; 075import org.ametys.core.user.CurrentUserProvider; 076import org.ametys.core.user.UserIdentity; 077import org.ametys.core.user.UserManager; 078import org.ametys.core.util.DateUtils; 079import org.ametys.core.util.LambdaUtils; 080import org.ametys.plugins.core.impl.schedule.DefaultRunnable; 081import org.ametys.plugins.core.user.UserHelper; 082import org.ametys.runtime.config.Config; 083import org.ametys.runtime.i18n.I18nizableText; 084import org.ametys.runtime.model.DefinitionContext; 085import org.ametys.runtime.model.ElementDefinition; 086import org.ametys.runtime.model.type.ElementType; 087import org.ametys.runtime.plugin.component.AbstractLogEnabled; 088import org.ametys.runtime.workspace.WorkspaceMatcher; 089 090/** 091 * The scheduler component 092 */ 093public class Scheduler extends AbstractLogEnabled implements Component, Initializable, Disposable, Serviceable, Contextualizable 094{ 095 /** The Avalon Role */ 096 public static final String ROLE = Scheduler.class.getName(); 097 098 /** The group name for jobs */ 099 public static final String JOB_GROUP = "runtime.job"; 100 /** The group name for triggers */ 101 public static final String TRIGGER_GROUP = "runtime.trigger"; 102 /** The key for the id of the schedulable to execute */ 103 public static final String KEY_SCHEDULABLE_ID = "schedulableId"; 104 /** The key for the runnable id */ 105 public static final String KEY_RUNNABLE_ID = "id"; 106 /** The key for the runnable label */ 107 public static final String KEY_RUNNABLE_LABEL = "label"; 108 /** The key for the runnable description */ 109 public static final String KEY_RUNNABLE_DESCRIPTION = "description"; 110 /** The key for the runnable fire process property */ 111 public static final String KEY_RUNNABLE_FIRE_PROCESS = "fireProcess"; 112 /** The key for 'run at startup' jobs indicating if the job has already been executed and is now completed */ 113 public static final String KEY_RUNNABLE_STARTUP_COMPLETED = "runAtStartupCompleted"; 114 /** The key for the runnable cron expression */ 115 public static final String KEY_RUNNABLE_CRON = "cron"; 116 /** The key for the runnable removable property */ 117 public static final String KEY_RUNNABLE_REMOVABLE = "removable"; 118 /** The key for the runnable modifiable property */ 119 public static final String KEY_RUNNABLE_MODIFIABLE = "modifiable"; 120 /** The key for the runnable deactivatable property */ 121 public static final String KEY_RUNNABLE_DEACTIVATABLE = "deactivatable"; 122 /** The key for the runnable volatile property */ 123 public static final String KEY_RUNNABLE_VOLATILE = "volatile"; 124 /** The key to retrieve the {@link UserIdentity} */ 125 public static final String KEY_RUNNABLE_USERIDENTITY = "userIdentity"; 126 /** The prefix for the parameter values of the runnable job in the job data map */ 127 public static final String PARAM_VALUES_PREFIX = "parameterValues#"; 128 /** Name of the parameter holding the datasource id for Quartz */ 129 public static final String DATASOURCE_CONFIG_NAME = "runtime.scheduler.datasource"; 130 131 /** The name of the configuration file for Quartz */ 132 private static final String __QUARTZ_CONFIG_FILE_NAME = "quartz.properties"; 133 /** The id of the right to execute actions on tasks */ 134 private static final String __RIGHT_SCHEDULER = "CORE_Rights_TaskScheduler"; 135 136 /** The service manager */ 137 protected ServiceManager _manager; 138 /** The context */ 139 protected Context _context; 140 /** The extension point for {@link Runnable}s */ 141 protected RunnableExtensionPoint _runnableEP; 142 /** The extension point for {@link Schedulable}s */ 143 protected SchedulableExtensionPoint _schedulableEP; 144 /** The Quartz scheduler */ 145 protected org.quartz.Scheduler _scheduler; 146 /** The manager for SQL datasources */ 147 protected SQLDataSourceManager _sqlDataSourceManager; 148 /** The source resolver */ 149 protected SourceResolver _sourceResolver; 150 /** The rights manager */ 151 protected RightManager _rightManager; 152 /** The provider of current user */ 153 protected CurrentUserProvider _currentUserProvider; 154 /** The sql database type ep */ 155 protected SQLDatabaseTypeExtensionPoint _sqlDatabaseTypeEP; 156 /** The user manager */ 157 protected UserManager _userManager; 158 /** The user helper */ 159 protected UserHelper _userHelper; 160 161 @Override 162 public void contextualize(Context context) throws ContextException 163 { 164 _context = context; 165 } 166 167 @Override 168 public void service(ServiceManager manager) throws ServiceException 169 { 170 _manager = manager; 171 _runnableEP = (RunnableExtensionPoint) manager.lookup(RunnableExtensionPoint.ROLE); 172 _schedulableEP = (SchedulableExtensionPoint) manager.lookup(SchedulableExtensionPoint.ROLE); 173 _sqlDataSourceManager = (SQLDataSourceManager) manager.lookup(SQLDataSourceManager.ROLE); 174 _sqlDatabaseTypeEP = (SQLDatabaseTypeExtensionPoint) manager.lookup(SQLDatabaseTypeExtensionPoint.ROLE); 175 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 176 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 177 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 178 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 179 _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE); 180 181 // Ensure statics methods will be available during initialize 182 manager.lookup(ConnectionHelper.ROLE); 183 } 184 185 @Override 186 public void initialize() throws Exception 187 { 188 // Be sure the tables are created 189 String dsId = Config.getInstance().getValue(DATASOURCE_CONFIG_NAME); 190 _checkAndCreateTables(dsId); 191 192 // Get info about datasource 193 Map<String, Object> dsParameters = _sqlDataSourceManager.getDataSourceDefinition(dsId).getParameters(); 194 String url = (String) dsParameters.get("url"); 195 String user = (String) dsParameters.get("user"); 196 String password = (String) dsParameters.get("password"); 197 String dbtype = (String) dsParameters.get("dbtype"); 198 SQLDatabaseType sqlDbType = _sqlDatabaseTypeEP.getExtension(dbtype); 199 200 String dbType = ConnectionHelper.getDatabaseType(url); 201 String dsName = "quartzDb"; 202 203 // Set the properties for Quartz 204 Properties props = new Properties(); 205 props.load(Scheduler.class.getResourceAsStream(__QUARTZ_CONFIG_FILE_NAME)); 206 props.setProperty("org.quartz.jobStore.dataSource", dsName); 207 props.setProperty(StdSchedulerFactory.PROP_DATASOURCE_PREFIX + "." + dsName + "." + PoolingConnectionProvider.DB_DRIVER, sqlDbType.getDriver()); 208 props.setProperty(StdSchedulerFactory.PROP_DATASOURCE_PREFIX + "." + dsName + "." + PoolingConnectionProvider.DB_URL, url); 209 props.setProperty(StdSchedulerFactory.PROP_DATASOURCE_PREFIX + "." + dsName + "." + PoolingConnectionProvider.DB_USER, user); 210 props.setProperty(StdSchedulerFactory.PROP_DATASOURCE_PREFIX + "." + dsName + "." + PoolingConnectionProvider.DB_PASSWORD, password); 211 props.setProperty("org.quartz.jobStore.driverDelegateClass", _getDriverDelegateClass(dbType)); 212 StdSchedulerFactory factory = new StdSchedulerFactory(props); 213 _scheduler = factory.getScheduler(); 214 215 // Initialize AmetysJob class 216 AmetysJob.initialize(_manager, _context); 217 } 218 219 /* copy/paste of SqlTablesInit#init() because SqlTablesInit comes too late */ 220 private void _checkAndCreateTables(String dataSourceId) 221 { 222 // Test and create tables 223 try 224 { 225 SQLScriptHelper.createTableIfNotExists(dataSourceId, "QRTZ_JOB_DETAILS", "plugin:core://scripts/%s/quartz.sql", _sourceResolver); 226 } 227 catch (Exception e) 228 { 229 String errorMsg = String.format("Error during SQL tables initialization for data source id: '%s'.", StringUtils.defaultString(dataSourceId)); 230 getLogger().error(errorMsg, e); 231 } 232 } 233 234 private String _getDriverDelegateClass(String dbType) 235 { 236 switch (dbType) 237 { 238 case ConnectionHelper.DATABASE_HSQLDB: 239 return "org.quartz.impl.jdbcjobstore.HSQLDBDelegate"; 240 case ConnectionHelper.DATABASE_POSTGRES: 241 return "org.quartz.impl.jdbcjobstore.PostgreSQLDelegate"; 242 default: 243 return "org.quartz.impl.jdbcjobstore.StdJDBCDelegate"; 244 } 245 } 246 247 /** 248 * Starts the Quartz scheduler. Only call this method once ! 249 * @throws SchedulerException if an error occured 250 */ 251 public void start() throws SchedulerException 252 { 253 _removeVolatileJobs(); 254 _scheduleConfigurableJobs(); 255 _triggerRunAtStartupJobs(); 256 257 getLogger().info("Start up the scheduler"); 258 _scheduler.start(); 259 } 260 261 /** 262 * Get the Quartz scheduler 263 * @return the scheduler 264 */ 265 public org.quartz.Scheduler getScheduler() 266 { 267 return _scheduler; 268 } 269 270 private void _removeVolatileJobs() throws SchedulerException 271 { 272 for (JobKey jobKey : getJobs()) 273 { 274 JobDataMap jobDataMap = _scheduler.getJobDetail(jobKey).getJobDataMap(); 275 if (jobDataMap.getBoolean(KEY_RUNNABLE_VOLATILE)) 276 { 277 _scheduler.deleteJob(jobKey); 278 } 279 } 280 } 281 282 private void _scheduleConfigurableJobs() 283 { 284 try 285 { 286 for (String runnableId : _runnableEP.getExtensionsIds()) 287 { 288 JobKey jobKey = new JobKey(runnableId, JOB_GROUP); 289 if (_scheduler.checkExists(jobKey)) 290 { 291 // Check if exists, but it should never do since the configurable are told volatile and were removed before 292 _scheduler.deleteJob(jobKey); 293 } 294 Runnable runnable = _runnableEP.getExtension(runnableId); 295 296 scheduleJob(runnable); 297 } 298 } 299 catch (SchedulerException e) 300 { 301 getLogger().error("An exception occured during the scheduling of configurable runnables", e); 302 } 303 } 304 305 private void _triggerRunAtStartupJobs() throws SchedulerException 306 { 307 for (JobKey jobKey : getJobs()) 308 { 309 JobDataMap jobDataMap = _scheduler.getJobDetail(jobKey).getJobDataMap(); 310 if (FireProcess.STARTUP.toString().equals(jobDataMap.getString(KEY_RUNNABLE_FIRE_PROCESS)) 311 && !jobDataMap.getBoolean(KEY_RUNNABLE_STARTUP_COMPLETED)) 312 { 313 _scheduler.triggerJob(jobKey); 314 } 315 } 316 } 317 318 /** 319 * Schedules a job 320 * @param runnable The runnable job to schedule 321 * @throws SchedulerException if the Job or Trigger cannot be added to the Scheduler, or there is an internal Scheduler error. 322 */ 323 public void scheduleJob(Runnable runnable) throws SchedulerException 324 { 325 String runnableId = runnable.getId(); 326 String schedulableId = runnable.getSchedulableId(); 327 328 // Schedule the job 329 JobDetail jobDetail = null; 330 JobBuilder jobBuilder = JobBuilder.newJob(AmetysJob.class) 331 .withIdentity(runnableId, JOB_GROUP) 332 .usingJobData(KEY_SCHEDULABLE_ID, schedulableId) 333 .usingJobData(_runnableToJobDataMap(runnable)); 334 335 Date firstTime = null; 336 switch (runnable.getFireProcess()) 337 { 338 case NEVER: 339 // will never fire 340 jobDetail = jobBuilder.storeDurably().build(); 341 342 _scheduler.addJob(jobDetail, true); 343 getLogger().info("{} has been scheduled to never run", jobDetail.getKey()); 344 break; 345 case STARTUP: 346 // will fire at next startup 347 jobDetail = jobBuilder.storeDurably().build(); 348 349 _scheduler.addJob(jobDetail, true); 350 getLogger().info("{} has been scheduled to run at next startup of the application", jobDetail.getKey()); 351 break; 352 case NOW: 353 // will be triggered now 354 jobDetail = jobBuilder.storeDurably().build(); 355 356 Trigger simpleTrigger = TriggerBuilder.newTrigger() 357 .withIdentity(runnableId, TRIGGER_GROUP) 358 .startNow() 359 .build(); 360 361 firstTime = _scheduler.scheduleJob(jobDetail, simpleTrigger); 362 getLogger().info("{} has been scheduled to run as soon as possible", jobDetail.getKey()); 363 break; 364 case CRON: 365 default: 366 // based on a cron trigger 367 jobDetail = jobBuilder.storeDurably().build(); 368 369 CronScheduleBuilder schedBuilder = CronScheduleBuilder.cronSchedule(runnable.getCronExpression()); 370 schedBuilder.inTimeZone(TimeZone.getTimeZone(ZoneOffset.UTC)); 371 372 switch (runnable.getMisfirePolicy()) 373 { 374 case IGNORE: 375 schedBuilder.withMisfireHandlingInstructionIgnoreMisfires(); 376 break; 377 case FIRE_ONCE: 378 schedBuilder.withMisfireHandlingInstructionFireAndProceed(); 379 break; 380 case DO_NOTHING: 381 default: 382 schedBuilder.withMisfireHandlingInstructionDoNothing(); 383 break; 384 } 385 CronTrigger cronTrigger = TriggerBuilder.newTrigger() 386 .withIdentity(runnableId, TRIGGER_GROUP) 387 .startNow() 388 .withSchedule(schedBuilder) 389 .build(); 390 391 firstTime = _scheduler.scheduleJob(jobDetail, cronTrigger); 392 getLogger().info("{} has been scheduled to run at: {} and repeat based on expression: {}", jobDetail.getKey(), firstTime, cronTrigger.getCronExpression()); 393 break; 394 } 395 } 396 397 private JobDataMap _runnableToJobDataMap(Runnable runnable) 398 { 399 Map<String, Object> result = new HashMap<>(); 400 result.put(KEY_RUNNABLE_ID, runnable.getId()); 401 result.put(KEY_RUNNABLE_LABEL, I18nizableText.i18nizableTextToString(runnable.getLabel())); 402 result.put(KEY_RUNNABLE_DESCRIPTION, I18nizableText.i18nizableTextToString(runnable.getDescription())); 403 result.put(KEY_RUNNABLE_FIRE_PROCESS, runnable.getFireProcess().toString()); 404 result.put(KEY_RUNNABLE_CRON, runnable.getCronExpression()); 405 result.put(KEY_RUNNABLE_REMOVABLE, runnable.isRemovable()); 406 result.put(KEY_RUNNABLE_MODIFIABLE, runnable.isModifiable()); 407 result.put(KEY_RUNNABLE_DEACTIVATABLE, runnable.isDeactivatable()); 408 result.put(KEY_RUNNABLE_VOLATILE, runnable.isVolatile()); 409 result.put(KEY_RUNNABLE_USERIDENTITY, UserIdentity.userIdentityToString(runnable.getUserIdentity())); 410 411 412 // parameter values 413 for (Object paramId : runnable.getParameterValues().keySet()) 414 { 415 result.put(PARAM_VALUES_PREFIX + paramId, runnable.getParameterValues().get(paramId)); 416 } 417 418 return new JobDataMap(result); 419 } 420 421 /** 422 * Gets the jobs of the Quartz scheduler 423 * @return the jobs 424 * @throws SchedulerException if an error occured 425 */ 426 public Set<JobKey> getJobs() throws SchedulerException 427 { 428 return _scheduler.getJobKeys(GroupMatcher.jobGroupEquals(JOB_GROUP)); 429 } 430 431 /** 432 * Gets tasks information 433 * @param taskIds The ids of the tasks 434 * @return The tasks information 435 * @throws SchedulerException if an error occurred 436 */ 437 @Callable(right = __RIGHT_SCHEDULER, context = "/admin") 438 public List<Map<String, Object>> getTasksInformation(List<String> taskIds) throws SchedulerException 439 { 440 List<Map<String, Object>> result = new ArrayList<>(); 441 442 for (JobKey jobKey : getJobs()) 443 { 444 String id = jobKey.getName(); 445 if (taskIds.contains(id)) 446 { 447 Map<String, Object> task = new HashMap<>(); 448 JobDataMap jobDataMap = _scheduler.getJobDetail(jobKey).getJobDataMap(); 449 task.put("id", id); 450 task.put("modifiable", jobDataMap.getBoolean(KEY_RUNNABLE_MODIFIABLE)); 451 task.put("removable", jobDataMap.getBoolean(KEY_RUNNABLE_REMOVABLE)); 452 task.put("deactivatable", jobDataMap.getBoolean(KEY_RUNNABLE_DEACTIVATABLE)); 453 result.add(task); 454 } 455 } 456 457 return result; 458 } 459 460 /** 461 * Gets all the scheduled tasks 462 * @return the scheduled tasks 463 * @throws Exception if an error occured 464 */ 465 public List<Map<String, Object>> getTasksAsJson() throws Exception 466 { 467 List<JobKey> executingJobs = _getExecutingJobs(); 468 469 return getJobs().stream() 470 .map(LambdaUtils.wrap(key-> _jobToJson(key, executingJobs))) 471 .filter(Objects::nonNull) 472 .collect(Collectors.toList()); 473 } 474 475 private List<JobKey> _getExecutingJobs() throws SchedulerException 476 { 477 return _scheduler.getCurrentlyExecutingJobs().stream() 478 .map(JobExecutionContext::getJobDetail) 479 .map(JobDetail::getKey) 480 .collect(Collectors.toList()); 481 } 482 483 private Map<String, Object> _jobToJson(JobKey jobKey, List<JobKey> executingJobs) throws Exception 484 { 485 Map<String, Object> result = new HashMap<>(); 486 487 JobDataMap jobDataMap = _scheduler.getJobDetail(jobKey).getJobDataMap(); 488 489 String id = jobDataMap.getString(KEY_RUNNABLE_ID); 490 result.put("id", id); 491 492 String schedulableId = jobDataMap.getString(KEY_SCHEDULABLE_ID); 493 Schedulable schedulable; 494 if (_schedulableEP.hasExtension(schedulableId)) 495 { 496 schedulable = _schedulableEP.getExtension(schedulableId); 497 } 498 else 499 { 500 // Abort trying to JSONify this job, as its associated schedulable does not exist anymore 501 // It will not be displayed in the UI tool 502 getLogger().warn("The Runnable '{}' is associated to a Schedulable that does not exist anymore ({}).", id, schedulableId); 503 return null; 504 } 505 506 result.put("label", I18nizableText.stringToI18nizableText((String) jobDataMap.get(KEY_RUNNABLE_LABEL))); 507 result.put("description", I18nizableText.stringToI18nizableText((String) jobDataMap.get(KEY_RUNNABLE_DESCRIPTION))); 508 String fireProcess = jobDataMap.getString(KEY_RUNNABLE_FIRE_PROCESS); 509 result.put("fireProcess", fireProcess); 510 result.put("launchUser", Optional.ofNullable(jobDataMap.getString(KEY_RUNNABLE_USERIDENTITY)) // Test if KEY_RUNNABLE_USERIDENTITY is null 511 .map(UserIdentity::stringToUserIdentity) // Get the UserIdentity 512 .map(_userManager::getUser) // Get the User from the UserIdentity 513 .map(u -> _userHelper.user2json(u, true)) // Transform the User in JSON 514 .orElse(Collections.EMPTY_MAP)); // Return an empty map if we get an Optionnal.EMPTY 515 result.put("removable", jobDataMap.getBoolean(KEY_RUNNABLE_REMOVABLE)); 516 result.put("modifiable", jobDataMap.getBoolean(KEY_RUNNABLE_MODIFIABLE)); 517 result.put("deactivatable", jobDataMap.getBoolean(KEY_RUNNABLE_DEACTIVATABLE)); 518 result.put("lastDuration", jobDataMap.get(AmetysJob.KEY_LAST_DURATION)); 519 result.put("success", jobDataMap.get(AmetysJob.KEY_SUCCESS)); 520 result.put("running", _isRunningJob(jobKey, executingJobs)); 521 522 result.put("schedulable", _schedulableToJson(schedulable, false)); 523 524 Long previousFireTimeMillis = null; 525 String cronExpression = null; 526 TriggerKey triggerKey = new TriggerKey(jobKey.getName(), TRIGGER_GROUP); 527 if (_scheduler.checkExists(triggerKey)) 528 { 529 Trigger trigger = _scheduler.getTrigger(triggerKey); 530 _triggerToJson(trigger, result); 531 if (trigger instanceof CronTrigger) 532 { 533 cronExpression = ((CronTrigger) trigger).getCronExpression(); 534 } 535 result.put("completed", false); 536 Date previousFireTime = trigger.getPreviousFireTime(); 537 if (previousFireTime != null) 538 { 539 previousFireTimeMillis = previousFireTime.getTime(); 540 } 541 } 542 else 543 { 544 result.put("enabled", true); 545 result.put("completed", !Runnable.FireProcess.STARTUP.toString().equals(fireProcess) || jobDataMap.getBoolean(KEY_RUNNABLE_STARTUP_COMPLETED)); 546 } 547 548 if (cronExpression == null && Runnable.FireProcess.CRON.toString().equals(fireProcess)) 549 { 550 // CRON based job, but no trigger (because no next execution), so get the cron expr in the job data map 551 cronExpression = jobDataMap.getString(KEY_RUNNABLE_CRON); 552 } 553 result.put("cronExpression", cronExpression); 554 555 if (previousFireTimeMillis == null && jobDataMap.containsKey(AmetysJob.KEY_PREVIOUS_FIRE_TIME)) 556 { 557 previousFireTimeMillis = (Long) jobDataMap.get(AmetysJob.KEY_PREVIOUS_FIRE_TIME); 558 } 559 560 result.put("previousFireTime", previousFireTimeMillis != null ? DateUtils.dateToString(new Date(previousFireTimeMillis)) : null); 561 562 return result; 563 } 564 565 private boolean _isRunningJob(JobKey jobKey, List<JobKey> executingJobs) 566 { 567 return executingJobs.contains(jobKey); 568 } 569 570 private void _triggerToJson(Trigger trigger, Map<String, Object> result) throws Exception 571 { 572 result.put("enabled", !TriggerState.PAUSED.equals(_scheduler.getTriggerState(trigger.getKey()))); 573 result.put("nextFireTime", org.ametys.core.util.DateUtils.dateToString(trigger.getNextFireTime())); 574 } 575 576 private Map<String, Object> _schedulableToJson(Schedulable schedulable, boolean isEdition) throws Exception 577 { 578 Map<String, Object> result = new HashMap<>(); 579 result.put("id", schedulable.getId()); 580 result.put("label", schedulable.getLabel()); 581 result.put("description", schedulable.getDescription()); 582 result.put("iconGlyph", schedulable.getIconGlyph()); 583 result.put("iconSmall", schedulable.getIconSmall()); 584 result.put("iconMedium", schedulable.getIconMedium()); 585 result.put("iconLarge", schedulable.getIconLarge()); 586 result.put("private", schedulable.isPrivate()); 587 588 Map<String, Object> params = new LinkedHashMap<>(); 589 for (String paramId : schedulable.getParameters().keySet()) 590 { 591 params.put(schedulable.getId() + "$" + paramId, schedulable.getParameters().get(paramId).toJSON(DefinitionContext.newInstance().withEdition(isEdition))); 592 } 593 result.put("parameters", params); 594 595 return result; 596 } 597 598 /** 599 * Gets the configuration for creating/editing a runnable (so returns all the schedulables and their parameters) 600 * @return A map containing information about what is needed to create/edit a runnable 601 * @throws Exception If an error occurs. 602 */ 603 @Callable(right = __RIGHT_SCHEDULER, context = "/admin") 604 public Map<String, Object> getEditionConfiguration() throws Exception 605 { 606 Map<String, Object> result = new HashMap<>(); 607 608 List<Map<String, Object>> fireProcesses = new ArrayList<>(); 609 for (FireProcess fireProcessVal : FireProcess.values()) 610 { 611 if (fireProcessVal.equals(FireProcess.NEVER)) 612 { 613 // do not display the 'NEVER' option 614 continue; 615 } 616 Map<String, Object> fireProcess = new LinkedHashMap<>(); 617 fireProcess.put("value", fireProcessVal.toString()); 618 String i18nKey = "PLUGINS_CORE_UI_TASKS_DIALOG_FIRE_PROCESS_OPTION_" + fireProcessVal.toString() + "_LABEL"; 619 fireProcess.put("label", new I18nizableText("plugin.core-ui", i18nKey)); 620 fireProcesses.add(fireProcess); 621 } 622 result.put("fireProcesses", fireProcesses); 623 624 List<Object> schedulables = new ArrayList<>(); 625 for (String schedulableId : _schedulableEP.getExtensionsIds()) 626 { 627 Schedulable schedulable = _schedulableEP.getExtension(schedulableId); 628 if (!schedulable.isPrivate()) 629 { 630 schedulables.add(_schedulableToJson(schedulable, true)); 631 } 632 } 633 result.put("schedulables", schedulables); 634 635 return result; 636 } 637 638 /** 639 * Gets the parameters of a given schedulable 640 * @param schedulableId The id of the schedulable 641 * @return A map containing parameters (prefixed) of the given schedulable 642 * @throws Exception If an error occurs. 643 */ 644 @Callable 645 public Map<String, Object> getParameters(String schedulableId) throws Exception 646 { 647 Schedulable schedulable = _schedulableEP.getExtension(schedulableId); 648 Map<String, Object> params = new LinkedHashMap<>(); 649 for (String paramId : schedulable.getParameters().keySet()) 650 { 651 params.put(schedulable.getId() + "$" + paramId, schedulable.getParameters().get(paramId).toJSON(DefinitionContext.newInstance().withEdition(true))); 652 } 653 return params; 654 } 655 656 /** 657 * Gets the values of the parameters of the given task 658 * @param id The id of the task 659 * @return The values of the parameters 660 */ 661 @Callable(right = __RIGHT_SCHEDULER, context = "/admin") 662 public Map<String, Object> getParameterValues(String id) 663 { 664 Map<String, Object> result = new HashMap<>(); 665 try 666 { 667 JobDetail jobDetail = _scheduler.getJobDetail(new JobKey(id, JOB_GROUP)); 668 JobDataMap jobDataMap = jobDetail.getJobDataMap(); 669 result.put("id", id); 670 result.put("label", I18nizableText.stringToI18nizableText((String) jobDataMap.get(KEY_RUNNABLE_LABEL))); 671 result.put("description", I18nizableText.stringToI18nizableText((String) jobDataMap.get(KEY_RUNNABLE_DESCRIPTION))); 672 result.put("fireProcess", jobDataMap.getString(KEY_RUNNABLE_FIRE_PROCESS)); 673 result.put("cron", jobDataMap.getString(KEY_RUNNABLE_CRON)); 674 String schedulableId = jobDataMap.getString(KEY_SCHEDULABLE_ID); 675 result.put("schedulableId", schedulableId); 676 result.put("launchUser", jobDataMap.getString(KEY_RUNNABLE_USERIDENTITY)); 677 Map<String, Object> params = new HashMap<>(); 678 result.put("params", params); 679 for (String param : jobDataMap.keySet()) 680 { 681 if (param.startsWith(PARAM_VALUES_PREFIX)) 682 { 683 params.put(schedulableId + "$" + param.substring(PARAM_VALUES_PREFIX.length()), jobDataMap.get(param)); 684 } 685 } 686 return result; 687 } 688 catch (SchedulerException e) 689 { 690 getLogger().error("An error occured when trying to retrieve the parameter values of the task " + id, e); 691 result.put("error", "scheduler-error"); 692 return result; 693 } 694 } 695 696 /** 697 * Returns true if the given task is modifiable. 698 * @param id The id of the task 699 * @return true if the given task is modifiable. 700 * @throws SchedulerException if an error occured 701 */ 702 @Callable(right = __RIGHT_SCHEDULER, context = "/admin") 703 public Map<String, Object> isModifiable(String id) throws SchedulerException 704 { 705 Map<String, Object> result = new HashMap<>(); 706 707 JobDetail jobDetail = _scheduler.getJobDetail(new JobKey(id, JOB_GROUP)); 708 if (jobDetail == null) 709 { 710 // Does not exist anymore 711 result.put("error", "not-found"); 712 return result; 713 } 714 JobDataMap jobDataMap = jobDetail.getJobDataMap(); 715 result.put("modifiable", jobDataMap.getBoolean(KEY_RUNNABLE_MODIFIABLE)); 716 return result; 717 } 718 719 /** 720 * Adds a new task. 721 * @param label The label 722 * @param description The description 723 * @param fireProcess the fire process 724 * @param cron The cron expression 725 * @param schedulableId The id of the schedulable model 726 * @param params The values of the parameters 727 * @return A result map 728 * @throws SchedulerException if an error occured 729 */ 730 @Callable 731 public Map<String, Object> add(String label, String description, String fireProcess, String cron, String schedulableId, Map<String, String> params) throws SchedulerException 732 { 733 Request request = ContextHelper.getRequest(_context); 734 String workspaceName = (String) request.getAttribute(WorkspaceMatcher.WORKSPACE_NAME); 735 UserIdentity taskUser = _currentUserProvider.getUser(); 736 if (taskUser == null && !workspaceName.equals("admin")) 737 { 738 Map<String, Object> result = new HashMap<>(); 739 result.put("error", "invalid-schedulable"); 740 return result; 741 } 742 return add(label, description, fireProcess, cron, schedulableId, _userHelper.user2json(taskUser), params); 743 } 744 745 /** 746 * Adds a new task. 747 * @param label The label 748 * @param description The description 749 * @param fireProcess the fire process 750 * @param cron The cron expression 751 * @param schedulableId The id of the schedulable model 752 * @param launchUser The user to launch the task 753 * @param params The values of the parameters 754 * @return A result map 755 * @throws SchedulerException if an error occured 756 */ 757 @Callable(right = __RIGHT_SCHEDULER, context = "/admin") 758 public Map<String, Object> add(String label, String description, String fireProcess, String cron, String schedulableId, Map<String, Object> launchUser, Map<String, String> params) throws SchedulerException 759 { 760 Map<String, Object> result = new HashMap<>(); 761 762 if (_schedulableEP.getExtension(schedulableId) == null) 763 { 764 result.put("error", "invalid-schedulable"); 765 return result; 766 } 767 else if (_schedulableEP.getExtension(schedulableId).isPrivate()) 768 { 769 result.put("error", "private"); 770 return result; 771 } 772 773 String id = _generateUniqueId(label); 774 return _edit(id, label, description, FireProcess.valueOf(fireProcess.toUpperCase()), cron, schedulableId, false, launchUser, params); 775 } 776 777 private String _generateUniqueId(String label) throws SchedulerException 778 { 779 String value = label.toLowerCase().trim().replaceAll("[\\W_]", "-").replaceAll("-+", "-").replaceAll("^-", ""); 780 int i = 2; 781 String suffixedValue = value; 782 while (_scheduler.checkExists(new JobKey(suffixedValue, JOB_GROUP))) 783 { 784 suffixedValue = value + i; 785 i++; 786 } 787 788 return suffixedValue; 789 } 790 791 /** 792 * Edits the given task. 793 * @param id The id of the task 794 * @param label The label 795 * @param description The description 796 * @param fireProcess the fire process 797 * @param cron The cron expression 798 * @param launchUser The user to launch the task 799 * @param params The values of the parameters 800 * @return A result map 801 * @throws SchedulerException if an error occured 802 */ 803 @Callable(right = __RIGHT_SCHEDULER, context = "/admin") 804 public Map<String, Object> edit(String id, String label, String description, String fireProcess, String cron, Map<String, Object> launchUser, Map<String, String> params) throws SchedulerException 805 { 806 Map<String, Object> result = new HashMap<>(); 807 808 // Check if exist 809 JobDetail jobDetail = null; 810 JobKey jobKey = new JobKey(id, JOB_GROUP); 811 jobDetail = _scheduler.getJobDetail(jobKey); 812 if (jobDetail == null) 813 { 814 result.put("error", "not-found"); 815 return result; 816 } 817 818 // Check if modifiable 819 JobDataMap jobDataMap = jobDetail.getJobDataMap(); 820 boolean isModifiable = jobDataMap.getBoolean(KEY_RUNNABLE_MODIFIABLE); 821 if (!isModifiable) 822 { 823 result.put("error", "no-modifiable"); 824 return result; 825 } 826 827 // Remove the associated job as we will create another one 828 if (!_scheduler.deleteJob(jobKey)) 829 { 830 // Deletion did not succeed 831 result.put("error", "not-found"); 832 return result; 833 } 834 835 String schedulableId = jobDataMap.getString(KEY_SCHEDULABLE_ID); 836 boolean isVolatile = jobDataMap.getBoolean(KEY_RUNNABLE_VOLATILE); 837 return _edit(id, label, description, FireProcess.valueOf(fireProcess.toUpperCase()), cron, schedulableId, isVolatile, launchUser, params); 838 } 839 840 private Map<String, Object> _edit(String id, String label, String description, FireProcess fireProcess, String cron, String schedulableId, boolean isVolatile, Map<String, Object> launchUser, Map<String, String> params) 841 { 842 Map<String, Object> result = new HashMap<>(); 843 844 Map<String, Object> typedParams = _getTypedParams(params, schedulableId); 845 846 boolean deactivatable = !FireProcess.STARTUP.equals(fireProcess); // cannot disable a startup job as we do not attach any trigger to it 847 848 // If the user is empty, we get the current user 849 UserIdentity userIdentity = _userHelper.json2userIdentity(launchUser); 850 if (userIdentity == null) 851 { 852 userIdentity = _currentUserProvider.getUser(); 853 } 854 Runnable runnable = new DefaultRunnable(id, new I18nizableText(label), new I18nizableText(description), fireProcess, cron, schedulableId, true, true, deactivatable, null, isVolatile, userIdentity, typedParams); 855 856 try 857 { 858 scheduleJob(runnable); 859 } 860 catch (SchedulerException e) 861 { 862 getLogger().error("An error occured when trying to add/edit the task " + id, e); 863 result.put("error", "scheduler-error"); 864 return result; 865 } 866 867 result.put("id", id); 868 return result; 869 } 870 871 private Map<String, Object> _getTypedParams(Map<String, String> params, String schedulableId) 872 { 873 Map<String, Object> result = new HashMap<>(); 874 875 Map<String, ElementDefinition> declaredParams = _schedulableEP.getExtension(schedulableId).getParameters(); 876 for (String nameWithPrefix : params.keySet()) 877 { 878 String[] splitStr = nameWithPrefix.split("\\$", 2); 879 String prefix = splitStr[0]; 880 String paramName = splitStr[1]; 881 if (prefix.equals(schedulableId) && declaredParams.containsKey(paramName)) 882 { 883 String originalValue = params.get(nameWithPrefix); 884 885 ElementDefinition definition = declaredParams.get(paramName); 886 ElementType type = definition.getType(); 887 888 Object typedValue = type.castValue(originalValue); 889 result.put(paramName, typedValue); 890 } 891 else if (prefix.equals(schedulableId)) 892 { 893 getLogger().warn("The parameter {} is not declared in schedulable {}. It will be ignored", paramName, schedulableId); 894 } 895 } 896 897 return result; 898 } 899 900 /** 901 * Removes the given task. 902 * @param id The id of the task 903 * @return A result map 904 * @throws SchedulerException if an error occured 905 */ 906 @Callable(right = __RIGHT_SCHEDULER, context = "/admin") 907 public Map<String, Object> remove(String id) throws SchedulerException 908 { 909 Map<String, Object> result = new HashMap<>(); 910 JobKey jobKey = new JobKey(id, JOB_GROUP); 911 JobDetail jobDetail = null; 912 try 913 { 914 jobDetail = _scheduler.getJobDetail(jobKey); 915 } 916 catch (SchedulerException e) 917 { 918 getLogger().error("An error occured when trying to remove the task " + id, e); 919 result.put("error", "scheduler-error"); 920 return result; 921 } 922 if (jobDetail == null) 923 { 924 result.put("error", "not-found"); 925 return result; 926 } 927 else if (_isRunningJob(jobKey, _getExecutingJobs())) 928 { 929 result.put("error", "is-running"); 930 return result; 931 } 932 933 JobDataMap jobDataMap = jobDetail.getJobDataMap(); 934 boolean isRemovable = jobDataMap.getBoolean(KEY_RUNNABLE_REMOVABLE); 935 if (!isRemovable) 936 { 937 result.put("error", "no-removable"); 938 return result; 939 } 940 941 try 942 { 943 if (_scheduler.deleteJob(jobKey)) 944 { 945 result.put("id", id); 946 return result; 947 } 948 else 949 { 950 // Deletion did not succeed 951 result.put("error", "no-delete"); 952 return result; 953 } 954 } 955 catch (SchedulerException e) 956 { 957 getLogger().error("An error occured when trying to remove the task " + id, e); 958 result.put("error", "scheduler-error"); 959 return result; 960 } 961 } 962 963 /** 964 * Enables/disables the given task. 965 * @param id The id of the task 966 * @param enabled true to enable the task, false to disable it. 967 * @return A result map 968 */ 969 @Callable(right = __RIGHT_SCHEDULER, context = "/admin") 970 public Map<String, Object> enable(String id, boolean enabled) 971 { 972 Map<String, Object> result = new HashMap<>(); 973 974 JobKey jobKey = new JobKey(id, JOB_GROUP); 975 JobDetail jobDetail = null; 976 try 977 { 978 jobDetail = _scheduler.getJobDetail(jobKey); 979 } 980 catch (SchedulerException e) 981 { 982 getLogger().error("An error occured when trying to enable/disable the task " + id, e); 983 result.put("error", "scheduler-error"); 984 return result; 985 } 986 if (jobDetail == null) 987 { 988 result.put("error", "not-found"); 989 return result; 990 } 991 JobDataMap jobDataMap = jobDetail.getJobDataMap(); 992 boolean isDeactivatable = jobDataMap.getBoolean(KEY_RUNNABLE_DEACTIVATABLE); 993 if (!isDeactivatable) 994 { 995 result.put("error", "no-deactivatable"); 996 return result; 997 } 998 999 try 1000 { 1001 if (enabled) 1002 { 1003 _scheduler.resumeJob(jobKey); 1004 } 1005 else 1006 { 1007 _scheduler.pauseJob(jobKey); 1008 } 1009 } 1010 catch (SchedulerException e) 1011 { 1012 getLogger().error("An error occured when trying to enable/disable the task " + id, e); 1013 result.put("error", "scheduler-error"); 1014 return result; 1015 } 1016 1017 result.put("id", id); 1018 return result; 1019 } 1020 1021 /** 1022 * Removes the completed tasks 1023 * @return The information of deleted tasks 1024 * @throws SchedulerException if an error occured 1025 */ 1026 @Callable(right = __RIGHT_SCHEDULER, context = "/admin") 1027 public List<Map<String, Object>> removeCompletedTasks() throws SchedulerException 1028 { 1029 List<Map<String, Object>> targets = new ArrayList<>(); 1030 1031 List<JobKey> jobsToRemove = new ArrayList<>(); 1032 for (JobKey jobKey : getJobs()) 1033 { 1034 JobDetail jobDetail = _scheduler.getJobDetail(jobKey); 1035 JobDataMap jobDataMap = jobDetail.getJobDataMap(); 1036 if (!Runnable.FireProcess.STARTUP.toString().equals(jobDataMap.getString(KEY_RUNNABLE_FIRE_PROCESS)) && !_scheduler.checkExists(new TriggerKey(jobKey.getName(), TRIGGER_GROUP)) // no trigger left when CRON or NOW 1037 || Runnable.FireProcess.STARTUP.toString().equals(jobDataMap.getString(KEY_RUNNABLE_FIRE_PROCESS)) && jobDataMap.getBoolean(KEY_RUNNABLE_STARTUP_COMPLETED)) // explicitly said completed when STARTUP 1038 { 1039 jobsToRemove.add(jobKey); 1040 } 1041 } 1042 1043 targets.addAll(getTasksInformation(jobsToRemove.stream().map(JobKey::getName).collect(Collectors.toList()))); 1044 for (JobKey jobKey : jobsToRemove) 1045 { 1046 _scheduler.deleteJob(jobKey); 1047 } 1048 1049 return targets; 1050 } 1051 1052 /** 1053 * Returns the enabled state of the task. 1054 * @param id The id of the task 1055 * @return A result map 1056 */ 1057 @Callable(right = __RIGHT_SCHEDULER, context = "/admin") 1058 public Map<String, Object> isEnabled(String id) 1059 { 1060 Map<String, Object> result = new HashMap<>(); 1061 1062 try 1063 { 1064 TriggerState state = _scheduler.getTriggerState(new TriggerKey(id, TRIGGER_GROUP)); 1065 if (state == null) 1066 { 1067 // Does not exist anymore 1068 result.put("error", "not-found"); 1069 return result; 1070 } 1071 result.put("enabled", !TriggerState.PAUSED.equals(state)); 1072 } 1073 catch (SchedulerException e) 1074 { 1075 getLogger().error("An error occured when trying to retrieve the enable state of the task " + id, e); 1076 result.put("error", "scheduler-error"); 1077 return result; 1078 } 1079 return result; 1080 } 1081 1082 /** 1083 * Returns the running state of the task. 1084 * @param id The id of the task 1085 * @return A result map 1086 */ 1087 @Callable(right = __RIGHT_SCHEDULER, context = "/admin") 1088 public Map<String, Object> isRunning(String id) 1089 { 1090 Map<String, Object> result = new HashMap<>(); 1091 1092 try 1093 { 1094 result.put("running", _isRunningJob(new JobKey(id, JOB_GROUP), _getExecutingJobs())); 1095 } 1096 catch (SchedulerException e) 1097 { 1098 getLogger().error("An error occured when trying to retrieve the running state of the task " + id, e); 1099 result.put("error", "scheduler-error"); 1100 } 1101 return result; 1102 } 1103 1104 /** 1105 * Gets all the schedulables (private or not) 1106 * @return the schedulables 1107 */ 1108 @Callable(right = __RIGHT_SCHEDULER, context = "/admin") 1109 public List<Map<String, Object>> getSchedulables() 1110 { 1111 List<Map<String, Object>> result = new ArrayList<>(); 1112 1113 for (String schedulableId : _schedulableEP.getExtensionsIds()) 1114 { 1115 Schedulable schedulable = _schedulableEP.getExtension(schedulableId); 1116 Map<String, Object> map = new HashMap<>(); 1117 map.put("id", schedulable.getId()); 1118 map.put("text", schedulable.getLabel()); 1119 result.add(map); 1120 } 1121 1122 return result; 1123 } 1124 1125 @Override 1126 public void dispose() 1127 { 1128 try 1129 { 1130 _scheduler.shutdown(); 1131 AmetysJob.dispose(); 1132 } 1133 catch (SchedulerException e) 1134 { 1135 getLogger().error("Fail to shutdown scheduler", e); 1136 } 1137 } 1138}