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