/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.camel.component.quartz; import java.util.Date; import java.util.Map; import org.apache.camel.CamelExchangeException; import org.apache.camel.Exchange; import org.apache.camel.Processor; import org.apache.camel.Producer; import org.apache.camel.ShutdownableService; import org.apache.camel.impl.DefaultEndpoint; import org.apache.camel.processor.loadbalancer.LoadBalancer; import org.apache.camel.processor.loadbalancer.RoundRobinLoadBalancer; import org.apache.camel.spi.Metadata; import org.apache.camel.spi.UriEndpoint; import org.apache.camel.spi.UriParam; import org.apache.camel.spi.UriPath; import org.apache.camel.support.ServiceSupport; import org.apache.camel.util.ObjectHelper; import org.apache.camel.util.ServiceHelper; import org.quartz.JobDetail; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.SchedulerException; import org.quartz.Trigger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Provides a scheduled delivery of messages using the Quartz 1.x scheduler. */ @UriEndpoint(firstVersion = "1.0.0", scheme = "quartz", title = "Quartz", syntax = "quartz:groupName/timerName", consumerOnly = true, consumerClass = QuartzConsumer.class, label = "scheduling") public class QuartzEndpoint extends DefaultEndpoint implements ShutdownableService { private static final Logger LOG = LoggerFactory.getLogger(QuartzEndpoint.class); private LoadBalancer loadBalancer; private Trigger trigger; private JobDetail jobDetail = new JobDetail(); private volatile boolean started; @UriPath(defaultValue = "Camel") private String groupName; @UriPath @Metadata(required = "true") private String timerName; @UriParam private String cron; @UriParam private boolean stateful; @UriParam(defaultValue = "true") private boolean deleteJob = true; @UriParam private boolean pauseJob; @UriParam private boolean fireNow; @UriParam private int startDelayedSeconds; @UriParam private boolean usingFixedCamelContextName; @UriParam(label = "advanced", prefix = "trigger.", multiValue = true) private Map<String, Object> triggerParameters; @UriParam(label = "advanced", prefix = "job.", multiValue = true) private Map<String, Object> jobParameters; public QuartzEndpoint(final String endpointUri, final QuartzComponent component) { super(endpointUri, component); getJobDetail().setName("quartz-" + getId()); } public void addTrigger(final Trigger trigger, final JobDetail detail) throws SchedulerException { // lets default the trigger name to the job name if (trigger.getName() == null) { trigger.setName(detail.getName()); } // lets default the trigger group to the job group if (trigger.getGroup() == null) { trigger.setGroup(detail.getGroup()); } // default start time to now if not specified if (trigger.getStartTime() == null) { trigger.setStartTime(new Date()); } detail.getJobDataMap().put(QuartzConstants.QUARTZ_ENDPOINT_URI, getEndpointUri()); if (isUsingFixedCamelContextName()) { detail.getJobDataMap().put(QuartzConstants.QUARTZ_CAMEL_CONTEXT_NAME, getCamelContext().getName()); } else { // must use management name as it should be unique in the same JVM detail.getJobDataMap().put(QuartzConstants.QUARTZ_CAMEL_CONTEXT_NAME, QuartzHelper.getQuartzContextName(getCamelContext())); } if (detail.getJobClass() == null) { detail.setJobClass(isStateful() ? StatefulCamelJob.class : CamelJob.class); } if (detail.getName() == null) { detail.setName(getJobName()); } getComponent().addJob(detail, trigger); } public void pauseTrigger(final Trigger trigger) throws SchedulerException { getComponent().pauseJob(trigger); } public void deleteTrigger(final Trigger trigger) throws SchedulerException { getComponent().deleteJob(trigger.getName(), trigger.getGroup()); } /** * This method is invoked when a Quartz job is fired. * * @param jobExecutionContext the Quartz Job context */ public void onJobExecute(final JobExecutionContext jobExecutionContext) throws JobExecutionException { boolean run = true; LoadBalancer balancer = getLoadBalancer(); if (balancer instanceof ServiceSupport) { run = ((ServiceSupport) balancer).isRunAllowed(); } if (!run) { // quartz scheduler could potential trigger during a route has been shutdown LOG.warn("Cannot execute Quartz Job with context: " + jobExecutionContext + " because processor is not started: " + balancer); return; } LOG.debug("Firing Quartz Job with context: {}", jobExecutionContext); Exchange exchange = createExchange(jobExecutionContext); try { balancer.process(exchange); if (exchange.getException() != null) { // propagate the exception back to Quartz throw new JobExecutionException(exchange.getException()); } } catch (Exception e) { // log the error LOG.error(CamelExchangeException.createExceptionMessage("Error processing exchange", exchange, e)); // and rethrow to let quartz handle it if (e instanceof JobExecutionException) { throw (JobExecutionException) e; } throw new JobExecutionException(e); } } public Exchange createExchange(final JobExecutionContext jobExecutionContext) { Exchange exchange = createExchange(); exchange.setIn(new QuartzMessage(exchange, jobExecutionContext)); return exchange; } public Producer createProducer() throws Exception { throw new UnsupportedOperationException("You cannot send messages to this endpoint"); } public QuartzConsumer createConsumer(Processor processor) throws Exception { QuartzConsumer answer = new QuartzConsumer(this, processor); configureConsumer(answer); return answer; } @Override protected String createEndpointUri() { return "quartz://" + getTrigger().getGroup() + "/" + getTrigger().getName(); } protected String getJobName() { return getJobDetail().getName(); } // Properties // ------------------------------------------------------------------------- @Override public QuartzComponent getComponent() { return (QuartzComponent) super.getComponent(); } public boolean isSingleton() { return true; } public LoadBalancer getLoadBalancer() { return loadBalancer; } public String getGroupName() { return groupName; } /** * The quartz group name to use. The combination of group name and timer name should be unique. */ public void setGroupName(String groupName) { this.groupName = groupName; } public String getTimerName() { return timerName; } /** * The quartz timer name to use. The combination of group name and timer name should be unique. */ public void setTimerName(String timerName) { this.timerName = timerName; } public String getCron() { return cron; } /** * Specifies a cron expression to define when to trigger. */ public void setCron(String cron) { this.cron = cron; } public void setLoadBalancer(final LoadBalancer loadBalancer) { this.loadBalancer = loadBalancer; } public JobDetail getJobDetail() { return jobDetail; } public void setJobDetail(final JobDetail jobDetail) { this.jobDetail = jobDetail; } public Trigger getTrigger() { return trigger; } public void setTrigger(final Trigger trigger) { this.trigger = trigger; } public boolean isStateful() { return this.stateful; } /** * Uses a Quartz StatefulJob instead of the default job. */ public void setStateful(final boolean stateful) { this.stateful = stateful; } public boolean isDeleteJob() { return deleteJob; } /** * If set to true, then the trigger automatically delete when route stop. * Else if set to false, it will remain in scheduler. When set to false, it will also mean user may reuse * pre-configured trigger with camel Uri. Just ensure the names match. * Notice you cannot have both deleteJob and pauseJob set to true. */ public void setDeleteJob(boolean deleteJob) { this.deleteJob = deleteJob; } public boolean isPauseJob() { return pauseJob; } /** * If set to true, then the trigger automatically pauses when route stop. * Else if set to false, it will remain in scheduler. When set to false, it will also mean user may reuse * pre-configured trigger with camel Uri. Just ensure the names match. * Notice you cannot have both deleteJob and pauseJob set to true. */ public void setPauseJob(boolean pauseJob) { this.pauseJob = pauseJob; } public boolean isFireNow() { return fireNow; } /** * Whether to fire the scheduler asap when its started using the simple trigger (this option does not support cron) */ public void setFireNow(boolean fireNow) { this.fireNow = fireNow; } public int getStartDelayedSeconds() { return startDelayedSeconds; } /** * Seconds to wait before starting the quartz scheduler. */ public void setStartDelayedSeconds(int startDelayedSeconds) { this.startDelayedSeconds = startDelayedSeconds; } public boolean isUsingFixedCamelContextName() { return usingFixedCamelContextName; } /** * If it is true, JobDataMap uses the CamelContext name directly to reference the CamelContext, * if it is false, JobDataMap uses use the CamelContext management name which could be changed during the deploy time. */ public void setUsingFixedCamelContextName(boolean usingFixedCamelContextName) { this.usingFixedCamelContextName = usingFixedCamelContextName; } public Map<String, Object> getTriggerParameters() { return triggerParameters; } /** * To configure additional options on the trigger. */ public void setTriggerParameters(Map<String, Object> triggerParameters) { this.triggerParameters = triggerParameters; } public Map<String, Object> getJobParameters() { return jobParameters; } /** * To configure additional options on the job. */ public void setJobParameters(Map<String, Object> jobParameters) { this.jobParameters = jobParameters; } // Implementation methods // ------------------------------------------------------------------------- public synchronized void consumerStarted(final QuartzConsumer consumer) throws SchedulerException { ObjectHelper.notNull(trigger, "trigger"); LOG.debug("Adding consumer {}", consumer.getProcessor()); getLoadBalancer().addProcessor(consumer.getProcessor()); // if we have not yet added our default trigger, then lets do it if (!started) { addTrigger(getTrigger(), getJobDetail()); started = true; } } public synchronized void consumerStopped(final QuartzConsumer consumer) throws SchedulerException { ObjectHelper.notNull(trigger, "trigger"); if (started) { pauseTrigger(getTrigger()); started = false; } LOG.debug("Removing consumer {}", consumer.getProcessor()); getLoadBalancer().removeProcessor(consumer.getProcessor()); } protected LoadBalancer createLoadBalancer() { return new RoundRobinLoadBalancer(); } @Override protected void doStart() throws Exception { ObjectHelper.notNull(getComponent(), "QuartzComponent", this); if (loadBalancer == null) { loadBalancer = createLoadBalancer(); } ServiceHelper.startService(loadBalancer); if (isDeleteJob() && isPauseJob()) { throw new IllegalArgumentException("Cannot have both options deleteJob and pauseJob enabled"); } } @Override protected void doStop() throws Exception { ServiceHelper.stopService(loadBalancer); } @Override protected void doShutdown() throws Exception { ObjectHelper.notNull(trigger, "trigger"); if (isDeleteJob()) { deleteTrigger(getTrigger()); } else if (isPauseJob()) { pauseTrigger(getTrigger()); } } }