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