/* ==================================================================
* ManagedJobServiceRegistrationListener.java - Jul 22, 2013 9:42:57 AM
*
* Copyright 2007-2013 SolarNetwork.net Dev Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
* ==================================================================
*/
package net.solarnetwork.node.runtime;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.cm.ConfigurationEvent;
import org.osgi.service.cm.ConfigurationListener;
import org.quartz.CronTrigger;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.solarnetwork.node.job.ManagedTriggerAndJobDetail;
import net.solarnetwork.node.job.ServiceProvider;
/**
* An OSGi service registration listener for {@link ManagedTriggerAndJobDetail},
* so they can be automatically registered/unregistered with the job scheduler.
*
* <p>
* This class is designed to be registered as a listener for
* {@link ManagedTriggerAndJobDetail} beans registered as services. It only
* works with {@link CronTrigger} triggers. As
* {@link ManagedTriggerAndJobDetail} services are discovered, they will be
* scheduled to run in the configured {@link Scheduler}. As the services are
* removed, they will be un-scheduled. In this way bundles can export jobs to be
* run by the "core" {@code Scheduler} provided by this bundle.
* </p>
*
* <p>
* For example, this might be configured via OSGi Blueprint like this:
* </p>
*
* <pre>
* <reference-list id="managedJobs" interface="net.solarnetwork.node.job.ManagedTriggerAndJobDetail">
* <reference-listener bind-method="onBind" unbind-method="onUnbind">
* <bean class="net.solarnetwork.node.runtime.ManagedJobServiceRegistrationListener">
* <property name="scheduler" ref="scheduler"/>
* <property name="bundleContext" ref="bundleContext"/>
* </bean>
* </reference-listener>
* </reference-list>
* </pre>
*
* <p>
* This class also implements {@link ConfigurationListener} and will
* automatically register itself for {@link ConfigurationEvent} updates. If the
* cron expression associated with a registered job changes, the job will be
* automatically rescheduled with the new cron expression.
* </p>
*
* <p>
* As {@link ManagedTriggerAndJobDetail} implements {@link ServiceProvider} as
* well, any service configurations returned by
* {@link ServiceProvider#getServiceConfigurations()} will be automatically
* registered along with the job. When the job is unregistered the associated
* services will be unregistered as well.
* </p>
*
* <p>
* The configurable properties of this class are:
* </p>
*
* <dl class="class-properties">
* <dt>scheduler</dt>
* <dd>The Quartz {@link Scheduler} for scheduling and un-scheduling jobs with
* as {@link ManagedTriggerAndJobDetail} services are registered and
* un-registered.</dd>
*
* <dt>bundleContext</dt>
* <dd>The {@link BundleContext} to register for {@link ConfigurationEvent}
* notifications with.</dd>
* </dl>
*
* @author matt
* @version 2.0
*/
public class ManagedJobServiceRegistrationListener implements ConfigurationListener {
private Scheduler scheduler;
private BundleContext bundleContext;
private ServiceRegistration<ConfigurationListener> configurationListenerRef;
private final Map<String, List<ServiceRegistration<?>>> registeredServices = new HashMap<String, List<ServiceRegistration<?>>>();
private final Map<String, ManagedTriggerAndJobDetail> pidMap = new HashMap<String, ManagedTriggerAndJobDetail>();
private final Logger log = LoggerFactory.getLogger(getClass());
/**
* Call to close down this instance.
*/
public void finish() {
if ( configurationListenerRef != null ) {
configurationListenerRef.unregister();
configurationListenerRef = null;
}
}
/**
* Callback when a trigger has been registered.
*
* @param trigJob
* the trigger and job
* @param properties
* the service properties
*/
public void onBind(ManagedTriggerAndJobDetail trigJob, Map<String, ?> properties) {
log.debug("Bind called on [{}] with props {}", trigJob, properties);
if ( !(trigJob.getTrigger() instanceof CronTrigger) ) {
log.warn("Trigger {} is not a CronTrigger! Not scheduling.",
JobUtils.triggerKey(trigJob.getTrigger()));
return;
}
final CronTrigger origTrigger = (CronTrigger) trigJob.getTrigger();
final JobDetail origJobDetail = trigJob.getJobDetail();
final String pid = (String) properties.get(Constants.SERVICE_PID);
// rename job name and trigger name to account for instance
final CronTrigger instanceTrigger = origTrigger.getTriggerBuilder().withIdentity(pid).forJob(pid)
.build();
final JobDetail instanceJobDetail = origJobDetail.getJobBuilder().withIdentity(pid).build();
synchronized ( this ) {
if ( configurationListenerRef == null ) {
configurationListenerRef = bundleContext.registerService(ConfigurationListener.class,
this, null);
}
pidMap.put(pid, trigJob);
}
Collection<ServiceProvider.ServiceConfiguration> services = trigJob.getServiceConfigurations();
if ( services != null ) {
for ( ServiceProvider.ServiceConfiguration conf : services ) {
Object service = conf.getService();
String[] interfaces = conf.getInterfaces();
Dictionary<String, ?> props = dictionaryForMap(conf.getProperties());
if ( service != null && interfaces != null && interfaces.length > 0 ) {
log.debug("Registering managed service {} as {} with props {}", service,
Arrays.toString(interfaces), props);
ServiceRegistration<?> ref = bundleContext.registerService(interfaces, service,
props);
synchronized ( this ) {
List<ServiceRegistration<?>> refs = registeredServices.get(pid);
if ( refs == null ) {
refs = new ArrayList<ServiceRegistration<?>>(services.size());
registeredServices.put(pid, refs);
}
refs.add(ref);
}
}
}
}
JobUtils.scheduleCronJob(scheduler, instanceTrigger, instanceJobDetail,
instanceTrigger.getCronExpression(), instanceTrigger.getJobDataMap());
}
private Dictionary<String, ?> dictionaryForMap(Map<String, ?> map) {
if ( map == null ) {
return null;
}
return new Hashtable<String, Object>(map);
}
/**
* Callback when a trigger has been un-registered.
*
* @param trigJob
* the trigger and job
* @param properties
* the service properties
*/
public void onUnbind(ManagedTriggerAndJobDetail trigJob, Map<String, ?> properties) {
if ( trigJob == null ) {
// gemini blueprint calls this when availability="optional" and there are no services
return;
}
final String pid = (String) properties.get(Constants.SERVICE_PID);
try {
boolean deletedJob = scheduler.deleteJob(new JobKey(pid));
if ( deletedJob ) {
log.debug("Un-scheduled job {}", pid);
} else {
log.warn("Attempted to un-schedule job {} that wasn't found", pid);
}
} catch ( SchedulerException e ) {
log.error("Unable to un-schedule job " + trigJob);
throw new RuntimeException(e);
}
synchronized ( this ) {
pidMap.remove(pid);
List<ServiceRegistration<?>> refs = registeredServices.get(pid);
if ( refs != null ) {
for ( ServiceRegistration<?> reg : refs ) {
log.debug("Unregistering managed service " + reg);
reg.unregister();
}
registeredServices.remove(pid);
}
}
}
@Override
public void configurationEvent(ConfigurationEvent event) {
if ( event.getType() == ConfigurationEvent.CM_UPDATED ) {
final String pid = event.getPid();
ManagedTriggerAndJobDetail trigJob = null;
synchronized ( pidMap ) {
trigJob = pidMap.get(pid);
}
if ( trigJob == null ) {
return;
}
final CronTrigger origTrigger = (CronTrigger) trigJob.getTrigger();
final JobDetail origJobDetail = trigJob.getJobDetail();
// rename job name and trigger name to account for instance
final CronTrigger instanceTrigger = origTrigger.getTriggerBuilder().withIdentity(pid)
.forJob(pid).build();
final JobDetail instanceJobDetail = origJobDetail.getJobBuilder().withIdentity(pid).build();
// even though the cron expression is also updated by ConfigurationAdmin, it can happen in a different thread
// so it might not be updated yet so we must extract the current value from ConfigurationAdmin
String newCronExpression = origTrigger.getCronExpression();
JobDataMap newJobDataMap = (JobDataMap) origJobDetail.getJobDataMap().clone();
@SuppressWarnings("unchecked")
ServiceReference<ConfigurationAdmin> caRef = event.getReference();
ConfigurationAdmin ca = bundleContext.getService(caRef);
try {
Configuration config = ca.getConfiguration(pid, null);
@SuppressWarnings("unchecked")
Dictionary<String, ?> props = config.getProperties();
String propCronExpression = (String) props.get("trigger.cronExpression");
if ( propCronExpression != null ) {
newCronExpression = propCronExpression;
}
// get JobDataMap
Enumeration<String> keyEnum = props.keys();
Pattern pat = Pattern.compile("trigger\\.jobDataMap\\['([a-zA-Z0-9_]*)'\\].*");
while ( keyEnum.hasMoreElements() ) {
String key = keyEnum.nextElement();
Matcher m = pat.matcher(key);
if ( m.matches() ) {
newJobDataMap.put(m.group(1), props.get(key));
}
}
} catch ( IOException e ) {
log.warn("Exception processing configuration update event", e);
}
if ( newCronExpression != null ) {
JobUtils.scheduleCronJob(scheduler, instanceTrigger, instanceJobDetail,
newCronExpression, newJobDataMap);
}
}
}
public void setScheduler(Scheduler scheduler) {
this.scheduler = scheduler;
}
public void setBundleContext(BundleContext bundleContext) {
this.bundleContext = bundleContext;
}
}