/**
* (C) Copyright 2013 Jabylon (http://www.jabylon.org) and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
/**
*
*/
package org.jabylon.scheduler.internal;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.felix.scr.annotations.Service;
import org.eclipse.core.runtime.preferences.DefaultScope;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.INodeChangeListener;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.NodeChangeEvent;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.jabylon.cdo.connector.RepositoryConnector;
import org.jabylon.common.progress.ProgressService;
import org.jabylon.common.progress.Progression;
import org.jabylon.common.progress.RunnableWithProgress;
import org.jabylon.common.resolver.URIResolver;
import org.jabylon.common.util.ApplicationConstants;
import org.jabylon.common.util.AttachablePreferences;
import org.jabylon.common.util.PreferencesUtil;
import org.jabylon.scheduler.JobExecution;
import org.jabylon.scheduler.JobInstance;
import org.jabylon.scheduler.ScheduleServiceException;
import org.jabylon.scheduler.SchedulerService;
import org.osgi.service.prefs.BackingStoreException;
import org.osgi.service.prefs.Preferences;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.Job;
import org.quartz.JobBuilder;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Johannes Utzig (jutzig.dev@googlemail.com)
*
*/
@Component(immediate = true, enabled = true)
@Service({ ProgressService.class, SchedulerService.class })
public class JobRegistry implements INodeChangeListener, IPreferenceChangeListener, SchedulerService, ProgressService {
private Scheduler scheduler;
public static final String PLUGIN_ID = "org.jabylon.scheduler";
private static final Logger logger = LoggerFactory.getLogger(JobRegistry.class);
private AtomicLong oneTimeJobs = new AtomicLong();
@Reference
private RepositoryConnector repositoryConnector;
@Reference
private URIResolver uriResolver;
/**
* jobDefinitions contains the actual service. Exactly one per service
*/
@Reference(referenceInterface = JobExecution.class, cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE, policy = ReferencePolicy.DYNAMIC, bind = "bindJob", unbind = "unbindJob")
private Map<String, JobExecution> jobDefinitions = new ConcurrentHashMap<String, JobExecution>();
/**
* contains a mapping from job id to preference node that contains the
* settings. There can be multiple instances per service in jobDefinitions
* as long as the each have a different preferences context
*/
private Map<String, Preferences> jobInstances = new ConcurrentHashMap<String, Preferences>();
public JobRegistry() {
}
public void bindUriResolver(URIResolver resolver) {
this.uriResolver = resolver;
}
public void unbindUriResolver(URIResolver resolver) {
this.uriResolver = resolver;
}
public void bindRepositoryConnector(RepositoryConnector connector) {
this.repositoryConnector = connector;
}
public void unbindRepositoryConnector(RepositoryConnector connector) {
this.repositoryConnector = null;
}
public void bindJob(JobExecution execution, Map<String, Object> properties) {
Preferences prefs = PreferencesUtil.getNodeForJob(PreferencesUtil.workspaceScope(), execution.getID());
Preferences defaultPrefs = defaultsFor(execution.getID());
jobDefinitions.put(execution.getID(), execution);
Set<Entry<String, Object>> entrySet = properties.entrySet();
for (Entry<String, Object> entry : entrySet) {
defaultPrefs.put(entry.getKey(), entry.getValue().toString());
}
try {
updateJob(prefs);
} catch (SchedulerException e) {
logger.error("Failed to schedule job " + execution, e);
}
}
public void unbindJob(JobExecution execution) {
Iterator<Entry<String, Preferences>> iterator = jobInstances.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Preferences> entry = (Map.Entry<String, Preferences>) iterator.next();
Preferences value = entry.getValue();
if (execution.getID().equals(value.name())) {
iterator.remove();
removeJob(value.absolutePath());
}
}
AttachablePreferences prefs = new AttachablePreferences(PreferencesUtil.workspaceScope().node(ApplicationConstants.JOBS_NODE_NAME), execution.getID());
jobDefinitions.remove(execution.getID());
removeJob(prefs.absolutePath());
}
@Activate
public void activate() throws SchedulerException {
SchedulerFactory schedFact = new org.quartz.impl.StdSchedulerFactory();
scheduler = schedFact.getScheduler();
scheduler.start();
absorbNode(PreferencesUtil.rootScope());
for (Preferences jobDefinition : jobInstances.values()) {
updateJob(jobDefinition);
}
}
/**
* updates or creates a job according to the given preferences
*
* @param node
* @param jobID
* @throws SchedulerException
*/
public void updateJob(Preferences node) throws SchedulerException {
jobInstances.put(node.absolutePath(), node);
if (scheduler == null)
// not yet activated
return;
String jobID = node.absolutePath();
Preferences defaults = defaultsFor(node.name());
boolean active = node.getBoolean(JobExecution.PROP_JOB_ACTIVE, defaults.getBoolean(JobExecution.PROP_JOB_ACTIVE, false));
CronTrigger trigger = null;
try {
trigger = createSchedule(jobID, node);
} catch (Exception e) {
logger.error("Invalid cron expression for job " + node, e);
}
removeJob(jobID);
if (trigger != null && active) {
scheduler.scheduleJob(createJobDetails(node, jobID), trigger);
}
}
protected Preferences defaultsFor(String jobID) {
Preferences defaultPrefs = DefaultScope.INSTANCE.getNode("org.jabylon.scheduler");
return defaultPrefs.node(jobID);
}
private CronTrigger createSchedule(String jobID, Preferences prefs) {
Preferences defaults = defaultsFor(prefs.name());
String cron = prefs.get(JobExecution.PROP_JOB_SCHEDULE, defaults.get(JobExecution.PROP_JOB_SCHEDULE, null));
if (cron == null || cron.trim().isEmpty())
return null;
return TriggerBuilder.newTrigger().forJob(prefs.absolutePath()).withIdentity(prefs.absolutePath()).withSchedule(CronScheduleBuilder.cronSchedule(cron))
.build();
}
private JobDetail createJobDetails(Preferences element, String jobID) {
Preferences defaults = defaultsFor(element.name());
JobBuilder builder = JobBuilder.newJob(JabylonJob.class).withIdentity(element.absolutePath())
.withDescription(element.get(JobExecution.PROP_JOB_DESCRIPTION, defaults.get(JobExecution.PROP_JOB_DESCRIPTION, null))).storeDurably(true);
try {
String[] keys = element.keys();
for (String string : keys) {
builder.usingJobData(string, element.get(string, defaults.get(string, null)));
}
} catch (BackingStoreException e) {
logger.error("Failed to retrieve properties of node " + element, e);
}
JobDataMap extras = new JobDataMap();
extras.put(JabylonJob.CONNECTOR_KEY, repositoryConnector);
extras.put(JabylonJob.EXECUTION_KEY, jobDefinitions.get(element.name()));
extras.put(JabylonJob.DOMAIN_OBJECT_KEY, getDomainObject(element));
builder.usingJobData(extras);
return builder.build();
}
private JobDetail createOneShotJobDetails(RunnableWithProgress task, String id, String description) {
JobBuilder builder = JobBuilder.newJob(JabylonJob.class).withIdentity(new JobKey(id)).withDescription(description);
JobDataMap extras = new JobDataMap();
extras.put(JabylonJob.EXECUTION_KEY, new RunnableWithProgressWrapper(task, getScheduler(), id));
builder.usingJobData(extras);
return builder.build();
}
private Object getDomainObject(Preferences jobConfig) {
// up one node, and one more to leave the /jobs node
Preferences domainPrefs = jobConfig.parent().parent();
String domainPath = domainPrefs.absolutePath();
String prefix = InstanceScope.INSTANCE.getNode(ApplicationConstants.CONFIG_NODE).absolutePath();
String path = domainPath.substring(prefix.length(), domainPath.length());
return uriResolver.resolve(path);
}
@Deactivate
public void deactivate() throws SchedulerException {
scheduler.shutdown(true);
jobDefinitions.clear();
}
public Scheduler getScheduler() {
return scheduler;
}
@Override
public void preferenceChange(PreferenceChangeEvent event) {
if (hasChange(event)) {
if (event.getKey().equals(JobExecution.PROP_JOB_ACTIVE)) {
if (Boolean.FALSE.equals(event.getNewValue()))
removeJob(event.getNode().absolutePath());
}
try {
updateJob(event.getNode());
} catch (SchedulerException e) {
logger.error("Failed to update job " + event.getNode().absolutePath(), e);
}
}
}
private boolean hasChange(PreferenceChangeEvent event) {
Object oldValue = event.getOldValue();
Object newValue = event.getNewValue();
if (oldValue != null)
return !oldValue.equals(newValue);
return oldValue != newValue;
}
@Override
public void added(NodeChangeEvent event) {
Preferences child = event.getChild();
try {
absorbNode(child);
} catch (SchedulerException e) {
logger.error("Failed to absorb node " + child, e);
}
}
/**
* attaches listeners to the node if necessary and schedules a job if this
* node (or a child) contains jobs
*
* @param prefs
* @throws SchedulerException
*/
protected void absorbNode(Preferences prefs) throws SchedulerException {
IEclipsePreferences node = toEclipsePreferences(prefs);
if (node.parent().name().equals(ApplicationConstants.JOBS_NODE_NAME)) {
updateJob(node);
node.addPreferenceChangeListener(this);
} else {
node.addNodeChangeListener(this);
String[] children;
try {
children = node.childrenNames();
for (String child : children) {
absorbNode(node.node(child));
}
} catch (BackingStoreException e) {
logger.error("Failed to absorb node " + node, e);
}
}
}
@Override
public void removed(NodeChangeEvent event) {
Preferences child = event.getChild();
String jobID = child.name();
removeJob(jobID);
}
protected void removeJob(String jobID) {
if (scheduler == null)
return;
JobKey triggerKey = new JobKey(jobID);
try {
if (scheduler.isStarted() && !scheduler.isShutdown() && scheduler.checkExists(triggerKey)) {
scheduler.deleteJob(triggerKey);
}
} catch (SchedulerException e) {
logger.error("Failed to delete job " + jobID, e);
}
}
protected IEclipsePreferences toEclipsePreferences(Preferences node) {
if (node instanceof IEclipsePreferences) {
IEclipsePreferences pref = (IEclipsePreferences) node;
return pref;
}
return null;
}
@Override
public Date nextExecution(Preferences jobConfig) throws ScheduleServiceException {
return nextExecution(jobConfig.absolutePath());
}
public Date nextExecution(String jobID) throws ScheduleServiceException {
Preferences settings = jobInstances.get(jobID);
Preferences defaults = defaultsFor(jobID.substring(jobID.lastIndexOf("/") + 1));
if (settings != null && settings.getBoolean(JobExecution.PROP_JOB_ACTIVE, defaults.getBoolean(JobExecution.PROP_JOB_ACTIVE, false))) {
Trigger trigger;
try {
trigger = scheduler.getTrigger(new TriggerKey(jobID));
if (trigger != null)
return trigger.getNextFireTime();
} catch (SchedulerException e) {
throw new ScheduleServiceException(e);
}
}
return null;
}
@Override
public List<JobInstance> getRunningJobs() throws ScheduleServiceException {
List<JobInstance> jobInstances = new ArrayList<JobInstance>();
try {
List<JobExecutionContext> jobs = scheduler.getCurrentlyExecutingJobs();
for (JobExecutionContext context : jobs) {
Job instance = context.getJobInstance();
if (instance instanceof JobInstance) {
JobInstance jobInstance = (JobInstance) instance;
jobInstances.add(jobInstance);
}
}
} catch (Exception e) {
throw new ScheduleServiceException(e);
}
return jobInstances;
}
@Override
public void trigger(Preferences jobConfig) throws ScheduleServiceException {
try {
scheduler.triggerJob(new JobKey(jobConfig.absolutePath()));
} catch (SchedulerException e) {
throw new ScheduleServiceException(e);
}
}
@Override
public String schedule(RunnableWithProgress task, String description) {
long id = oneTimeJobs.getAndIncrement();
try {
scheduler.scheduleJob(createOneShotJobDetails(task, Long.toString(id), description), TriggerBuilder.newTrigger().startNow().build());
} catch (SchedulerException e) {
throw new RuntimeException("failed to schedule task", e);
}
return Long.toString(id);
}
@Override
public void shutdown() {
try {
deactivate();
} catch (SchedulerException e) {
logger.error("Shutdown failed", e);
}
}
@Override
public Progression progressionOf(String id) {
try {
JobKey key = new JobKey(id);
JobDetail jobDetail = getScheduler().getJobDetail(key);
List<JobExecutionContext> jobs = getScheduler().getCurrentlyExecutingJobs();
for (JobExecutionContext context : jobs) {
if (context.getJobDetail().getKey().equals(key)) {
JabylonJob job = (JabylonJob) context.getJobInstance();
return job.getProgress();
}
}
// the job is not started yet
if (jobDetail != null) {
ProgressionImpl fakeProgression = new ProgressionImpl();
return fakeProgression;
}
} catch (SchedulerException e) {
throw new RuntimeException("Failed to retrieve progression for id " + id, e);
}
return null;
}
@Override
public void cancel(String id) {
try {
JobKey key = new JobKey(id);
List<JobExecutionContext> jobs = getScheduler().getCurrentlyExecutingJobs();
for (JobExecutionContext context : jobs) {
if (context.getJobDetail().getKey().equals(key)) {
JabylonJob job = (JabylonJob) context.getJobInstance();
job.interrupt();
}
}
getScheduler().deleteJob(key);
} catch (SchedulerException e) {
throw new RuntimeException("Failed to retrieve progression for id " + id, e);
}
}
}