/* ==================================================================
* SimpleManagedTriggerAndJobDetail.java - Jul 21, 2013 1:08:03 PM
*
* 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.job;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.JobDetail;
import org.quartz.Trigger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.MessageSource;
import net.solarnetwork.node.settings.MappableSpecifier;
import net.solarnetwork.node.settings.SettingSpecifier;
import net.solarnetwork.node.settings.SettingSpecifierProvider;
import net.solarnetwork.node.settings.support.BasicCronExpressionSettingSpecifier;
import net.solarnetwork.node.settings.support.KeyedSmartQuotedTemplateMapper;
import net.solarnetwork.node.util.PrefixedMessageSource;
import net.solarnetwork.node.util.TemplatedMessageSource;
/**
* Extension of {@link SimpleTriggerAndJobDetail} that supports a
* {@link SettingSpecifierProvider} to manage the job at runtime.
*
* <p>
* There are two basic ways to configure this class. The first way involves
* configuring the {@code settingSpecifierProvider} property manually. In this
* case the keyed settings and message keys will be dynamically adjusted to work
* with a {@code jobDetail.jobDataMap['%s']} style pattern, where the {@code %s}
* will be replaced by the top-level property names of all keys.
* </p>
*
* <p>
* The second way involves leaving the {@code settingSpecifierProvider} not
* configured, and letting this class find the first available
* {@link SettingSpecifierProvider} instance in the configured
* {@link JobDetail#getJobDataMap()}. In this case the keyed settings provided
* by that {@link SettingSpecifierProvider} and all message keys will be
* prefixed with {@code jobDetail.jobDataMap['$mapKey'].} where {@code $mapKey}
* is the associated key of that object in the JobDataMap.
* </p>
*
* <p>
* In most situations the later approach is simplest to set up.
* </p>
*
* <p>
* The configurable properties of this class are:
* </p>
*
* <dl>
* <dt>settingSpecifierProvider</dt>
* <dd>The {@link SettingSpecifierProvider} that this class proxies all methods
* for. If not configured, then {@link JobDetail#getJobDataMap()} will be
* examined and the first value that implements {@link SettingSpecifierProvider}
* will be used.</dd>
*
* <dt>trigger</dt>
* <dd>The job trigger.</dd>
*
* <dt>jobDetail</dt>
* <dd>The job detail.</dd>
*
* <dt>serviceProviderConfigurations</dt>
* <dd>An optional mapping of {@code jobDetail} keys to associated
* {@link SimpleServiceProviderConfiguration} objects. The object on the given
* key will be extracted from the {@code jobDetail} map and that object will be
* returned as a {@link ServiceProvider.ServiceConfiguration} instance when
* {@link #getServiceConfigurations()} is called. This allows services used by
* the job to be exposed as services themselves in the runtime.</dd>
* </dl>
*
* @author matt
* @version 2.0
*/
public class SimpleManagedTriggerAndJobDetail implements ManagedTriggerAndJobDetail, ServiceProvider {
private static final Logger LOG = LoggerFactory.getLogger(SimpleManagedTriggerAndJobDetail.class);
/**
* The regular expression used to delegate properties to the delegate
* {@code settingSpecifierProvider}.
*/
public static final String JOB_DETAIL_PROPERTY_MAPPING_REGEX = "jobDetail\\.jobDataMap\\['([a-zA-Z0-9_]*)'\\](.*)";
private static final KeyedSmartQuotedTemplateMapper MAPPER = getMapper();
private SettingSpecifierProvider settingSpecifierProvider;
private Trigger trigger;
private String triggerCronExpression;
private JobDetail jobDetail;
private MessageSource messageSource;
private String simplePrefix;
private Map<String, SimpleServiceProviderConfiguration> serviceProviderConfigurations;
private static KeyedSmartQuotedTemplateMapper getMapper() {
KeyedSmartQuotedTemplateMapper result = new KeyedSmartQuotedTemplateMapper();
result.setTemplate("jobDetail.jobDataMap['%s']");
return result;
}
@Override
public String toString() {
return "ManagedTriggerAndJobDetail{job=" + jobDetail.getKey().getName() + ",trigger="
+ trigger.getKey().getName() + '}';
}
@Override
public Trigger getTrigger() {
Trigger result = trigger;
if ( triggerCronExpression != null && result instanceof CronTrigger ) {
try {
result = ((CronTrigger) result).getTriggerBuilder().withSchedule(
CronScheduleBuilder.cronScheduleNonvalidatedExpression(triggerCronExpression))
.build();
} catch ( ParseException e ) {
LOG.warn("Error parsing cron expression [{}]: {}. Trigger unchanged as {}",
triggerCronExpression, e.getMessage(),
((CronTrigger) result).getCronExpression());
}
}
return result;
}
public void setTrigger(Trigger trigger) {
this.trigger = trigger;
}
@Override
public JobDetail getJobDetail() {
return jobDetail;
}
public void setJobDetail(JobDetail jobDetail) {
this.jobDetail = jobDetail;
}
@Override
public String getSettingUID() {
return getSettingSpecifierProvider().getSettingUID();
}
@Override
public String getDisplayName() {
return getSettingSpecifierProvider().getDisplayName();
}
/**
* Get the trigger cron expression, if available.
*
* @return The cron expression, or <em>null</em> if not configured or the
* configured {@code Trigger} is not a {@code CronTrigger}.
* @since 2.0
*/
public String getTriggerCronExpression() {
if ( triggerCronExpression != null ) {
return triggerCronExpression;
}
if ( trigger instanceof CronTrigger ) {
return ((CronTrigger) trigger).getCronExpression();
}
return null;
}
/**
* Set a cron expression to use for the trigger.
*
* @param cronExpression
* The cron expression to use.
*/
public void setTriggerCronExpression(String cronExpression) {
this.triggerCronExpression = cronExpression;
}
@Override
public List<SettingSpecifier> getSettingSpecifiers() {
List<SettingSpecifier> result = new ArrayList<SettingSpecifier>();
if ( trigger instanceof CronTrigger ) {
CronTrigger ct = (CronTrigger) trigger;
result.add(new BasicCronExpressionSettingSpecifier("triggerCronExpression",
ct.getCronExpression()));
}
for ( SettingSpecifier spec : getSettingSpecifierProvider().getSettingSpecifiers() ) {
if ( spec instanceof MappableSpecifier ) {
MappableSpecifier keyedSpec = (MappableSpecifier) spec;
if ( simplePrefix != null ) {
result.add(keyedSpec.mappedTo(simplePrefix));
} else {
result.add(keyedSpec.mappedWithMapper(MAPPER));
}
} else {
result.add(spec);
}
}
return result;
}
@Override
public MessageSource getMessageSource() {
if ( messageSource == null ) {
TemplatedMessageSource tSource = new TemplatedMessageSource();
tSource.setDelegate(getSettingSpecifierProvider().getMessageSource());
tSource.setRegex(JOB_DETAIL_PROPERTY_MAPPING_REGEX);
messageSource = tSource;
}
return messageSource;
}
public SettingSpecifierProvider getSettingSpecifierProvider() {
if ( settingSpecifierProvider != null ) {
return settingSpecifierProvider;
}
for ( Map.Entry<String, Object> me : jobDetail.getJobDataMap().entrySet() ) {
Object o = me.getValue();
if ( o instanceof SettingSpecifierProvider ) {
SettingSpecifierProvider ssp = (SettingSpecifierProvider) o;
PrefixedMessageSource pSource = new PrefixedMessageSource();
String prefix = "jobDetail.jobDataMap['" + me.getKey() + "'].";
pSource.setPrefix(prefix);
pSource.setDelegate(ssp.getMessageSource());
messageSource = pSource;
settingSpecifierProvider = ssp;
simplePrefix = prefix;
break;
}
}
return settingSpecifierProvider;
}
@Override
public Collection<ServiceConfiguration> getServiceConfigurations() {
Collection<ServiceConfiguration> result = null;
if ( jobDetail != null && serviceProviderConfigurations != null
&& serviceProviderConfigurations.size() > 0 ) {
for ( Map.Entry<String, SimpleServiceProviderConfiguration> me : serviceProviderConfigurations
.entrySet() ) {
Object o = jobDetail.getJobDataMap().get(me.getKey());
if ( o != null ) {
SimpleServiceProviderConfiguration conf = me.getValue();
SimpleServiceProviderConfiguration ser = new SimpleServiceProviderConfiguration();
ser.setService(o);
ser.setInterfaces(conf.getInterfaces());
ser.setProperties(conf.getProperties());
if ( result == null ) {
result = new ArrayList<ServiceProvider.ServiceConfiguration>();
}
result.add(ser);
}
}
}
return result;
}
public void setSettingSpecifierProvider(SettingSpecifierProvider settingSpecifierProvider) {
this.settingSpecifierProvider = settingSpecifierProvider;
}
public void setServiceProviderConfigurations(
Map<String, SimpleServiceProviderConfiguration> serviceProviderConfigurations) {
this.serviceProviderConfigurations = serviceProviderConfigurations;
}
}