/** * 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.jooby.quartz; import static java.util.Objects.requireNonNull; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Consumer; import org.jooby.Env; import org.jooby.Jooby; import org.jooby.internal.quartz.JobExpander; import org.jooby.internal.quartz.QuartzProvider; import org.quartz.Job; import org.quartz.JobBuilder; import org.quartz.JobDetail; import org.quartz.JobExecutionContext; import org.quartz.JobKey; import org.quartz.Scheduler; import org.quartz.Trigger; import org.quartz.TriggerBuilder; import org.quartz.TriggerKey; import com.google.common.collect.Lists; import com.google.inject.Binder; import com.google.inject.TypeLiteral; import com.google.inject.name.Names; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; /** * Build and create a {@link Scheduler Quartz Scheduler} and {@link Job jobs}. * * <h1>usage</h1> * * <pre> * { * use(new Quartz().with(MyJob.class)); * } * </pre> * * Previous example will startup Quartz and schedule MyJob. * * <h1>jobs</h1> * <p> * A job can implement the {@link Job} interface as described in the <a * href="http://quartz-scheduler.org/documentation">Quartz documentation</a> * </p> * * <p> * If you prefer to not implement the {@link Job} interface, all you have to do is to annotated a * method with the {@link Scheduled} annotation. * </p> * <p> * By default, job name is set the class name or to the method name. Default group is set to the * package name of the job class. * </p> * * <h2>job methods</h2> * <p> * A job method must follow this rules: * </p> * * <ul> * <li>It must be a public method</li> * <li>Without a return value</li> * <li>Have ZERO arguments</li> * <li>or just ONE argument of type {@link JobExecutionContext}</li> * </ul> * * The next section will you show how to add a trigger to a job and some examples too. * * <h1>triggers</h1> * <p> * Trigger are defined by the {@link Scheduled} annotation. The annotation defines a single and * required attribute, which is basically a trigger expression or a reference to one. * </p> * * Examples: * <p> * Run every 5 minutes, start immediately and repeat for ever: * </p> * * <pre> * @Scheduled("5m") * * @Scheduled("5m; delay=0") * * @Scheduled("5m; delay=0; repeat=*") * </pre> * * Previous, expressions are identical. * * <p> * Run every 1 hour with an initial delay of 15 minutes for 10 times * </p> * * <pre> * @Scheduled("1h; delay=15m; repeat=10") * </pre> * * <p> * Fire at 12pm (noon) every day * </p> * * <pre> * 0 0 12 * * ? * </pre> * * <p> * Fire at 10:15am every day * </p> * * <pre> * 0 15 10 ? * * * </pre> * * <h1>grouping jobs together</h1> * <p> * If you have two or more jobs doing something similar, it is possible to group all them into one * single class: * </p> * * <pre> * public class MyJobs { * @Scheduled("5minutes") * public void job1() { * ... * } * * @Scheduled("1h") * public void job2() { * ... * } * } * </pre> * * <h1>dependency injection</h1> * <p> * Not much to add here, just let you know jobs are created by Guice. * </p> * * <pre> * public class MyJob { * * private A a; * * @Inject * public MyJob(A a) { * this.a = a; * } * * @Scheduled("5minutes") * public void doWork() { * this.a.doWork(); * } * } * </pre> * * <p> * Injecting a {@link Scheduler} * </p> * * <pre> * public class MyJobManager { * * private Scheduler scheduler; * * @Inject * public MyJobManager(Scheduler scheduler) { * this.scheduler = scheduler; * } * } * </pre> * * <h1>configuration</h1> * <p> * Example: Setting max number of threads * </p> * * <pre> * # application.conf * org.quartz.threadPool.threadCount = 1 # default is number of available processors * </pre> * <p> * Configuration follows the <a href="http://quartz-scheduler.org/documentation">Quartz * documentation</a>. The only difference is that you need to put add the properties on your * <code>*.conf</code> file, NOT in a custom <code>quartz.properties</code> file. * </p> * * <h2>jdbc store</h2> * <p> * Jdbc Store is fully supported but it depends on the <code>jooby-jdbc</code> module. So, in order * to use the Jdbc Store you need to follow these steps: * </p> * * <p> * 1. Install the Jdbc module: * </p> * * <pre> * { * use(new Jdbc()); * use(new Quartz(MyJob.class)); * } * </pre> * * <p> * 2. Set the quartz properties: * </p> * * <pre> * org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX * org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate * org.quartz.jobStore.dataSource = db * </pre> * * <h1>adding jobs programmatically</h1> * <p> * When {@link Scheduled} isn't not enough and/or if you prefer to build jobs manually, you can try * one of the available alternatives. * </p> * * <p> * Example 1: build the trigger and use default job naming * </p> * * <pre> * { * use(new Quartz() * .with(MyJob.class, trigger {@literal ->} { * trigger * .withSchedule(withIntervalInDays(3)) * .startAt(futureDate(10, MINUTES)); * }) * ); * } * </pre> * * <p> * Example 2: build the job, the trigger and use default job naming * </p> * * <pre> * { * use(new Quartz() * .with(MyJob.class, (job, trigger) {@literal ->} { * job.withDescription("etc..."); * * trigger * .withSchedule(withIntervalInDays(3)) * .startAt(futureDate(10, MINUTES)); * }) * ); * } * </pre> * * <p> * Example 3: build and set everything from scratch * </p> * * <pre> * { * use(new Quartz() * .with( * newJob(MyJob.class).withDescription("etc...") * .build(), * newTrigger() * .withSchedule(withIntervalInDays(3)) * .startAt(futureDate(10, MINUTES)) * .build() * }) * ); * } * </pre> * * Enjoy it! * * @author edgar * @since 0.5.0 */ public class Quartz implements Jooby.Module { private List<Class<?>> jobs; private Map<JobDetail, Trigger> jobMap = new HashMap<>(); /** * Creates a new {@link Quartz} module. Optionally add some jobs. * * @param jobs Jobs to setup. Optional. * @see #with(Class) */ public Quartz(final Class<?>... jobs) { this.jobs = Lists.newArrayList(jobs); } /** * Schedule the provided job and trigger. * * @param job A job to schedule. * @param trigger A trigger for provided job. * @return This quartz instance. */ public Quartz with(final JobDetail job, final Trigger trigger) { requireNonNull(job, "Job is required."); requireNonNull(trigger, "Trigger is required."); jobMap.put(job, trigger); return this; } /** * Setup and schedule the provided job, it might implement a {@link Job} and the {@link Scheduled} * annotation must be present. * * @param jobClass A jobClass to setup and schedule. * @return This quartz instance. */ public Quartz with(final Class<?> jobClass) { jobs.add(jobClass); return this; } /** * Schedule the provided job and trigger. This method will setup a default name and group for * both. * * @param jobClass A jobClass to setup and schedule. * @param configurer A callback to setup the job and trigger. * @return This quartz instance. */ public Quartz with(final Class<? extends Job> jobClass, final BiConsumer<JobBuilder, TriggerBuilder<Trigger>> configurer) { requireNonNull(jobClass, "Job class is required."); JobBuilder job = JobBuilder.newJob(jobClass) .withIdentity( JobKey.jobKey(jobClass.getSimpleName(), jobClass.getPackage().getName()) ); TriggerBuilder<Trigger> trigger = TriggerBuilder.newTrigger() .withIdentity( TriggerKey.triggerKey(jobClass.getSimpleName(), jobClass.getPackage().getName()) ); configurer.accept(job, trigger); return with(job.build(), trigger.build()); } /** * Schedule the provided job and trigger. This method will setup a default name and group for * both. * * @param jobClass A jobClass to setup and schedule. * @param configurer A callback to setup the trigger. * @return This quartz instance. */ public Quartz with(final Class<? extends Job> jobClass, final Consumer<TriggerBuilder<Trigger>> configurer) { requireNonNull(jobClass, "Job class is required."); JobBuilder job = JobBuilder.newJob(jobClass) .withIdentity( JobKey.jobKey(jobClass.getSimpleName(), jobClass.getPackage().getName()) ); TriggerBuilder<Trigger> trigger = TriggerBuilder.newTrigger() .withIdentity( TriggerKey.triggerKey(jobClass.getSimpleName(), jobClass.getPackage().getName()) ); configurer.accept(trigger); return with(job.build(), trigger.build()); } @Override public void configure(final Env env, final Config config, final Binder binder) { jobMap.putAll(JobExpander.jobs(config, jobs)); binder.bind(Scheduler.class).toProvider(QuartzProvider.class).asEagerSingleton(); env.lifeCycle(QuartzProvider.class); binder.bind(new TypeLiteral<Map<JobDetail, Trigger>>() { }).annotatedWith(Names.named("org.quartz.jobs")).toInstance(jobMap); } @Override public Config config() { return ConfigFactory.parseResources(getClass(), "quartz.conf") .withFallback(ConfigFactory.parseResources(Job.class, "quartz.properties")); } }