/* =================================================================== * JobServiceRegistrationListener.java * * Created Dec 2, 2009 10:29:15 AM * * Copyright 2007-2009 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 * =================================================================== * $Id$ * =================================================================== */ package net.solarnetwork.node.runtime; import java.io.IOException; import java.util.Dictionary; import java.util.Enumeration; import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.TreeMap; 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.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.Trigger; import net.solarnetwork.node.job.TriggerAndJobDetail; import net.solarnetwork.node.settings.SettingSpecifierProvider; import net.solarnetwork.node.util.BaseServiceListener; import net.solarnetwork.node.util.RegisteredService; /** * An OSGi service registration listener for jobs, so they can be automatically * registered/unregistered with the job scheduler. * * <p> * This class is designed to be registered as a listener for * {@link TriggerAndJobDetail} beans registered as services. As * {@link TriggerAndJobDetail} services are discovered, they will be scheduled * to run in the configured {@link Scheduler}. As those 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> * This class will also register {@link JobSettingSpecifierProvider} for every * unique bundle symbolic name. This allows the settings UI to expose the * registered jobs as configurable components. * </p> * * <p> * For example, this might be configured via OSGi Blueprint like this: * </p> * * <pre> * <reference-list id="triggers" interface="net.solarnetwork.node.job.TriggerAndJobDetail"> * <reference-listener bind-method="onBind" unbind-method="onUnbind"> * <bean class="net.solarnetwork.node.runtime.JobServiceRegistrationListener"> * <property name="scheduler" ref="scheduler"/> * <property name="bundleContext" ref="bundleContext"/> * </bean> * </reference-listener> * </reference-list> * </pre> * * <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 TriggerAndJobDetail} services are registered and * un-registered.</dd> * </dl> * * @author matt * @version 2.0 * @see ManagedJobServiceRegistrationListener for alternative using * settings-based jobs */ public class JobServiceRegistrationListener extends BaseServiceListener<TriggerAndJobDetail, RegisteredService<TriggerAndJobDetail>> implements ConfigurationListener { private Scheduler scheduler; private ServiceRegistration<ConfigurationListener> configurationListenerRef; private final Map<String, JobSettingSpecifierProvider> providerMap = new TreeMap<String, JobSettingSpecifierProvider>(); private String pidForSymbolicName(String name) { return name + ".JOBS"; } /** * Callback when a trigger has been registered. * * @param trigJob * the trigger and job * @param properties * the service properties */ public void onBind(TriggerAndJobDetail trigJob, Map<String, ?> properties) { if ( log.isDebugEnabled() ) { log.debug("Bind called on [" + trigJob + "] with props " + properties); } JobDetail job = trigJob.getJobDetail(); Trigger trigger = trigJob.getTrigger(); final String pid = pidForSymbolicName((String) properties.get("Bundle-SymbolicName")); String cronExpression = null; String settingKey = null; JobSettingSpecifierProvider provider = null; synchronized ( providerMap ) { if ( pid != null ) { provider = providerMap.get(pid); if ( provider == null ) { provider = new JobSettingSpecifierProvider(pid, trigJob.getMessageSource()); providerMap.put(pid, provider); } if ( configurationListenerRef == null ) { configurationListenerRef = getBundleContext() .registerService(ConfigurationListener.class, this, null); } // check for ConfigurationAdmin cron setting for this trigger, // and // use that if available settingKey = JobUtils.triggerKey(trigger); if ( trigger instanceof CronTrigger ) { cronExpression = ((CronTrigger) trigger).getCronExpression(); ConfigurationAdmin ca = (ConfigurationAdmin) getBundleContext().getService( getBundleContext().getServiceReference(ConfigurationAdmin.class.getName())); if ( ca != null ) { try { Configuration conf = ca.getConfiguration(pid, null); if ( conf != null ) { @SuppressWarnings("unchecked") Dictionary<String, ?> props = conf.getProperties(); if ( props != null ) { String newCronExpression = (String) props.get(settingKey); if ( newCronExpression != null ) { cronExpression = newCronExpression; } } } } catch ( IOException e ) { log.warn("Unable to get configuration for {}", pid, e); } } } } } try { if ( cronExpression != null && settingKey != null ) { JobUtils.scheduleCronJob(scheduler, (CronTrigger) trigJob.getTrigger(), trigJob.getJobDetail(), cronExpression, null); if ( provider != null ) { provider.addSpecifier(trigJob); RegisteredService<TriggerAndJobDetail> rs = new RegisteredService<TriggerAndJobDetail>( trigJob, properties); Hashtable<String, Object> serviceProps = new Hashtable<String, Object>(); serviceProps.put("settingPid", provider.getSettingUID()); addRegisteredService(rs, provider, new String[] { SettingSpecifierProvider.class.getName() }, serviceProps); } } else { scheduler.scheduleJob(job, trigger); } } catch ( SchedulerException e ) { log.error("Error scheduling trigger {} for job {}", new Object[] { trigger.getKey().getName(), trigger.getJobKey().getName(), e }); } } /** * Callback when a trigger has been un-registered. * * @param trigJob * the trigger and job * @param properties * the service properties */ public void onUnbind(TriggerAndJobDetail trigJob, Map<String, ?> properties) { if ( trigJob == null ) { // gemini blueprint calls this when availability="optional" and there are no services return; } try { scheduler.deleteJob(trigJob.getJobDetail().getKey()); } catch ( SchedulerException e ) { log.error("Unable to un-schedule job " + trigJob); throw new RuntimeException(e); } removeRegisteredService(trigJob, properties); final String pid = pidForSymbolicName((String) properties.get("Bundle-SymbolicName")); JobSettingSpecifierProvider provider = null; synchronized ( providerMap ) { provider = providerMap.get(pid); if ( provider != null ) { provider.removeSpecifier(trigJob); } } } @Override public void configurationEvent(ConfigurationEvent event) { if ( event.getType() == ConfigurationEvent.CM_UPDATED ) { JobSettingSpecifierProvider provider = null; synchronized ( providerMap ) { provider = providerMap.get(event.getPid()); } if ( provider != null ) { @SuppressWarnings("unchecked") ServiceReference<ConfigurationAdmin> caRef = event.getReference(); ConfigurationAdmin ca = getBundleContext().getService(caRef); try { Configuration config = ca.getConfiguration(event.getPid(), null); @SuppressWarnings("unchecked") Dictionary<String, ?> props = config.getProperties(); log.debug("CA PID {} updated props: {}", event.getPid(), props); Enumeration<String> keys = props.keys(); while ( keys.hasMoreElements() ) { String key = keys.nextElement(); List<RegisteredService<TriggerAndJobDetail>> tjList = getRegisteredServices(); synchronized ( tjList ) { for ( RegisteredService<TriggerAndJobDetail> rs : tjList ) { TriggerAndJobDetail tj = rs.getConfig(); if ( key.equals(JobUtils.triggerKey(tj.getTrigger())) ) { JobUtils.scheduleCronJob(scheduler, (CronTrigger) tj.getTrigger(), tj.getJobDetail(), (String) props.get(key), null); } } } } } catch ( IOException e ) { log.warn("Exception processing configuration update event", e); } } } } public Scheduler getScheduler() { return scheduler; } public void setScheduler(Scheduler scheduler) { this.scheduler = scheduler; } }