From 13bea6e502e8775c1e36b18eadd165f13573df70 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 25 May 2016 16:32:25 +0200 Subject: [PATCH] implemented small scheduler engine --- pom.xml | 1 + .../java/sonia/scm/schedule/Scheduler.java | 65 ++++++ .../main/java/sonia/scm/schedule/Task.java | 47 ++++ scm-webapp/pom.xml | 12 + .../java/sonia/scm/ScmContextListener.java | 3 + .../main/java/sonia/scm/ScmServletModule.java | 5 + .../scm/schedule/InjectionEnabledJob.java | 90 +++++++ .../sonia/scm/schedule/QuartzScheduler.java | 175 ++++++++++++++ .../java/sonia/scm/schedule/QuartzTask.java | 68 ++++++ .../scm/schedule/InjectionEnabledJobTest.java | 168 +++++++++++++ .../scm/schedule/QuartzSchedulerTest.java | 220 ++++++++++++++++++ .../sonia/scm/schedule/QuartzTaskTest.java | 89 +++++++ 12 files changed, 943 insertions(+) create mode 100644 scm-core/src/main/java/sonia/scm/schedule/Scheduler.java create mode 100644 scm-core/src/main/java/sonia/scm/schedule/Task.java create mode 100644 scm-webapp/src/main/java/sonia/scm/schedule/InjectionEnabledJob.java create mode 100644 scm-webapp/src/main/java/sonia/scm/schedule/QuartzScheduler.java create mode 100644 scm-webapp/src/main/java/sonia/scm/schedule/QuartzTask.java create mode 100644 scm-webapp/src/test/java/sonia/scm/schedule/InjectionEnabledJobTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/schedule/QuartzSchedulerTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/schedule/QuartzTaskTest.java diff --git a/pom.xml b/pom.xml index 35e261328b..2a8d1a2f9b 100644 --- a/pom.xml +++ b/pom.xml @@ -435,6 +435,7 @@ 15.0 + 2.2.3 1.6 diff --git a/scm-core/src/main/java/sonia/scm/schedule/Scheduler.java b/scm-core/src/main/java/sonia/scm/schedule/Scheduler.java new file mode 100644 index 0000000000..dfad4baeed --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/schedule/Scheduler.java @@ -0,0 +1,65 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.schedule; + +import java.io.Closeable; + +/** + * Scheduler is able to run tasks on the future in a background thread. Task can be scheduled with cron like expression. + * Note: Task are always executed in an administrative context. + * @author Sebastian Sdorra + * @since 1.47 + */ +public interface Scheduler extends Closeable { + + /** + * Schedule a new task for future execution. + * + * @param expression cron expression + * @param runnable action + * + * @return cancelable task + */ + public Task schedule(String expression, Runnable runnable); + + /** + * Schedule a new task for future execution. The method will create a new instance of the runnable for every + * execution. The runnable can use injection. + * + * @param expression cron expression + * @param runnable action class + * + * @return cancelable task + */ + public Task schedule(String expression, Class runnable); + +} diff --git a/scm-core/src/main/java/sonia/scm/schedule/Task.java b/scm-core/src/main/java/sonia/scm/schedule/Task.java new file mode 100644 index 0000000000..dc601bcb82 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/schedule/Task.java @@ -0,0 +1,47 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.schedule; + +/** + * Tasks are executed in the future and can be running more than once. A task execution can be canceled. + * + * @author Sebastian Sdorra + * @since 1.47 + */ +public interface Task { + + /** + * Cancel task execution. + */ + public void cancel(); + +} diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index 0e98df13a0..aa26a109e0 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -190,6 +190,18 @@ ${guava.version} + + org.quartz-scheduler + quartz + ${quartz.version} + + + c3p0 + c3p0 + + + + diff --git a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java b/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java index 3845c985f7..2c2efff045 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java @@ -60,6 +60,7 @@ import java.util.List; import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; import sonia.scm.repository.HealthCheckContextListener; +import sonia.scm.schedule.Scheduler; /** * @@ -79,6 +80,8 @@ public class ScmContextListener extends GuiceServletContextListener { if ((globalInjector != null) &&!startupError) { + // close Scheduler + IOUtil.close(globalInjector.getInstance(Scheduler.class)); // close RepositoryManager IOUtil.close(globalInjector.getInstance(RepositoryManager.class)); diff --git a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java index 132bc617c3..813c296558 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java @@ -164,6 +164,8 @@ import sonia.scm.net.ahc.ContentTransformer; import sonia.scm.net.ahc.DefaultAdvancedHttpClient; import sonia.scm.net.ahc.JsonContentTransformer; import sonia.scm.net.ahc.XmlContentTransformer; +import sonia.scm.schedule.QuartzScheduler; +import sonia.scm.schedule.Scheduler; import sonia.scm.security.XsrfProtectionFilter; import sonia.scm.web.UserAgentParser; @@ -282,6 +284,9 @@ public class ScmServletModule extends ServletModule bind(PluginLoader.class).toInstance(pluginLoader); bind(PluginManager.class, DefaultPluginManager.class); + // bind scheduler + bind(Scheduler.class).to(QuartzScheduler.class); + // note CipherUtil uses an other generator bind(KeyGenerator.class).to(DefaultKeyGenerator.class); bind(CipherHandler.class).toInstance(cu.getCipherHandler()); diff --git a/scm-webapp/src/main/java/sonia/scm/schedule/InjectionEnabledJob.java b/scm-webapp/src/main/java/sonia/scm/schedule/InjectionEnabledJob.java new file mode 100644 index 0000000000..44ac5ae3ab --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/schedule/InjectionEnabledJob.java @@ -0,0 +1,90 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.schedule; + +import com.google.common.base.Preconditions; +import com.google.inject.Injector; +import com.google.inject.Provider; +import org.quartz.Job; +import org.quartz.JobDataMap; +import org.quartz.JobDetail; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.web.security.AdministrationContext; +import sonia.scm.web.security.PrivilegedAction; + +/** + * InjectionEnabledJob allows the execution of quartz jobs and enable injection on them. + * + * @author Sebastian Sdorra + * @since 1.47 + */ +public class InjectionEnabledJob implements Job { + + private static final Logger logger = LoggerFactory.getLogger(InjectionEnabledJob.class); + + @Override + public void execute(JobExecutionContext jec) throws JobExecutionException { + Preconditions.checkNotNull(jec, "execution context is null"); + + JobDetail detail = jec.getJobDetail(); + Preconditions.checkNotNull(detail, "job detail not provided"); + + JobDataMap dataMap = detail.getJobDataMap(); + Preconditions.checkNotNull(dataMap, "job detail does not contain data map"); + + Injector injector = (Injector) dataMap.get(Injector.class.getName()); + Preconditions.checkNotNull(injector, "data map does not contain injector"); + + final Provider runnableProvider = (Provider) dataMap.get(Runnable.class.getName()); + if (runnableProvider == null) { + throw new JobExecutionException("could not find runnable provider"); + } + + AdministrationContext ctx = injector.getInstance(AdministrationContext.class); + ctx.runAsAdmin(new PrivilegedAction() + { + @Override + public void run() + { + logger.trace("create runnable from provider"); + Runnable runnable = runnableProvider.get(); + logger.debug("execute injection enabled job {}", runnable.getClass()); + runnable.run(); + } + }); + } + + +} diff --git a/scm-webapp/src/main/java/sonia/scm/schedule/QuartzScheduler.java b/scm-webapp/src/main/java/sonia/scm/schedule/QuartzScheduler.java new file mode 100644 index 0000000000..5b46d438d8 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/schedule/QuartzScheduler.java @@ -0,0 +1,175 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.schedule; + +import com.google.common.base.Throwables; +import com.google.inject.Injector; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import java.io.IOException; +import javax.inject.Inject; +import org.quartz.CronScheduleBuilder; +import org.quartz.JobBuilder; +import org.quartz.JobDataMap; +import org.quartz.JobDetail; +import org.quartz.SchedulerException; +import org.quartz.Trigger; +import org.quartz.TriggerBuilder; +import org.quartz.impl.StdSchedulerFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.Initable; +import sonia.scm.SCMContextProvider; + +/** + * {@link Scheduler} which uses the quartz scheduler. + * + * @author Sebastian Sdorra + * @since 1.47 + * + * @see Quartz Job Scheduler + */ +@Singleton +public class QuartzScheduler implements Scheduler, Initable { + + private static final Logger logger = LoggerFactory.getLogger(QuartzScheduler.class); + + private final Injector injector; + private final org.quartz.Scheduler scheduler; + + /** + * Creates a new quartz scheduler. + * + * @param injector injector + */ + @Inject + public QuartzScheduler(Injector injector) + { + this.injector = injector; + + // get default scheduler + try { + scheduler = StdSchedulerFactory.getDefaultScheduler(); + } catch (SchedulerException ex) { + throw Throwables.propagate(ex); + } + } + + /** + * Creates a new quartz scheduler. This constructor is only for testing. + * + * @param injector injector + * @param scheduler quartz scheduler + */ + QuartzScheduler(Injector injector, org.quartz.Scheduler scheduler) + { + this.injector = injector; + this.scheduler = scheduler; + } + + @Override + public void init(SCMContextProvider context) + { + try + { + if (!scheduler.isStarted()) + { + scheduler.start(); + } + } + catch (SchedulerException ex) + { + logger.error("can not start scheduler", ex); + } + } + + @Override + public void close() throws IOException + { + try + { + if (scheduler.isStarted()){ + scheduler.shutdown(); + } + } + catch (SchedulerException ex) + { + logger.error("can not stop scheduler", ex); + } + } + + @Override + public Task schedule(String expression, final Runnable runnable) + { + return schedule(expression, new Provider(){ + @Override + public Runnable get() + { + return runnable; + } + }); + } + + @Override + public Task schedule(String expression, Class runnable) + { + return schedule(expression, injector.getProvider(runnable)); + } + + private Task schedule(String expression, Provider provider){ + // create data map with injection provider for InjectionEnabledJob + JobDataMap map = new JobDataMap(); + map.put(Runnable.class.getName(), provider); + map.put(Injector.class.getName(), injector); + + // create job detail for InjectionEnabledJob with the provider for the annotated class + JobDetail detail = JobBuilder.newJob(InjectionEnabledJob.class) + .usingJobData(map) + .build(); + + // create a trigger with the cron expression from the annotation + Trigger trigger = TriggerBuilder.newTrigger() + .forJob(detail) + .withSchedule(CronScheduleBuilder.cronSchedule(expression)) + .build(); + + try { + scheduler.scheduleJob(detail, trigger); + } catch (SchedulerException ex) { + throw Throwables.propagate(ex); + } + + return new QuartzTask(scheduler, trigger.getJobKey()); + } + + +} diff --git a/scm-webapp/src/main/java/sonia/scm/schedule/QuartzTask.java b/scm-webapp/src/main/java/sonia/scm/schedule/QuartzTask.java new file mode 100644 index 0000000000..a45790a2e9 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/schedule/QuartzTask.java @@ -0,0 +1,68 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.schedule; + +import com.google.common.base.Throwables; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; + +/** + * Task implementation for the {@link QuartzScheduler}. + * + * @author Sebastian Sdorra + */ +public class QuartzTask implements Task { + + private final org.quartz.Scheduler scheduler; + private final JobKey jobKey; + + QuartzTask(Scheduler scheduler, JobKey jobKey) + { + this.scheduler = scheduler; + this.jobKey = jobKey; + } + + @Override + public void cancel() + { + try + { + scheduler.deleteJob(jobKey); + } + catch (SchedulerException ex) + { + throw Throwables.propagate(ex); + } + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/schedule/InjectionEnabledJobTest.java b/scm-webapp/src/test/java/sonia/scm/schedule/InjectionEnabledJobTest.java new file mode 100644 index 0000000000..9545577c9f --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/schedule/InjectionEnabledJobTest.java @@ -0,0 +1,168 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.schedule; + +import com.google.inject.Injector; +import com.google.inject.Provider; +import org.junit.Test; +import static org.mockito.Mockito.*; +import org.junit.Rule; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; +import org.quartz.JobDataMap; +import org.quartz.JobDetail; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import sonia.scm.web.security.AdministrationContext; +import sonia.scm.web.security.PrivilegedAction; + +/** + * Unit tests for {@link InjectionEnabledJob}. + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class InjectionEnabledJobTest { + + @Mock + private Injector injector; + + @Mock + private JobDataMap dataMap; + + @Mock + private JobDetail detail; + + @Mock + private JobExecutionContext jec; + + @Mock + private Provider runnable; + + @Mock + private AdministrationContext context; + + @Rule + public ExpectedException expected = ExpectedException.none(); + + /** + * Tests {@link InjectionEnabledJob#execute(org.quartz.JobExecutionContext)} without context. + * + * @throws JobExecutionException + */ + @Test + public void testExecuteWithoutContext() throws JobExecutionException + { + expected.expect(NullPointerException.class); + expected.expectMessage("execution context"); + new InjectionEnabledJob().execute(null); + } + + /** + * Tests {@link InjectionEnabledJob#execute(org.quartz.JobExecutionContext)} without job detail. + * + * @throws JobExecutionException + */ + @Test + public void testExecuteWithoutJobDetail() throws JobExecutionException + { + expected.expect(NullPointerException.class); + expected.expectMessage("detail"); + new InjectionEnabledJob().execute(jec); + } + + /** + * Tests {@link InjectionEnabledJob#execute(org.quartz.JobExecutionContext)} without data map. + * + * @throws JobExecutionException + */ + @Test + public void testExecuteWithoutDataMap() throws JobExecutionException + { + when(jec.getJobDetail()).thenReturn(detail); + expected.expect(NullPointerException.class); + expected.expectMessage("data map"); + new InjectionEnabledJob().execute(jec); + } + + /** + * Tests {@link InjectionEnabledJob#execute(org.quartz.JobExecutionContext)} without injector. + * + * @throws JobExecutionException + */ + @Test + public void testExecuteWithoutInjector() throws JobExecutionException + { + when(jec.getJobDetail()).thenReturn(detail); + when(detail.getJobDataMap()).thenReturn(dataMap); + expected.expect(NullPointerException.class); + expected.expectMessage("injector"); + new InjectionEnabledJob().execute(jec); + } + + /** + * Tests {@link InjectionEnabledJob#execute(org.quartz.JobExecutionContext)} without runnable. + * + * @throws JobExecutionException + */ + @Test + public void testExecuteWithoutRunnable() throws JobExecutionException + { + when(jec.getJobDetail()).thenReturn(detail); + when(detail.getJobDataMap()).thenReturn(dataMap); + when(dataMap.get(Injector.class.getName())).thenReturn(injector); + expected.expect(JobExecutionException.class); + expected.expectMessage("runnable"); + new InjectionEnabledJob().execute(jec); + } + + /** + * Tests {@link InjectionEnabledJob#execute(org.quartz.JobExecutionContext)}. + * + * @throws JobExecutionException + */ + @Test + public void testExecute() throws JobExecutionException + { + when(jec.getJobDetail()).thenReturn(detail); + when(detail.getJobDataMap()).thenReturn(dataMap); + when(dataMap.get(Injector.class.getName())).thenReturn(injector); + when(dataMap.get(Runnable.class.getName())).thenReturn(runnable); + when(injector.getInstance(AdministrationContext.class)).thenReturn(context); + new InjectionEnabledJob().execute(jec); + verify(context).runAsAdmin(Mockito.any(PrivilegedAction.class)); + } + +} \ No newline at end of file diff --git a/scm-webapp/src/test/java/sonia/scm/schedule/QuartzSchedulerTest.java b/scm-webapp/src/test/java/sonia/scm/schedule/QuartzSchedulerTest.java new file mode 100644 index 0000000000..2f3dc86ea1 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/schedule/QuartzSchedulerTest.java @@ -0,0 +1,220 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.schedule; + +import com.google.inject.Injector; +import com.google.inject.Provider; +import java.io.IOException; +import org.junit.Test; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; +import static org.hamcrest.Matchers.*; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.quartz.CronTrigger; +import org.quartz.JobDetail; +import org.quartz.SchedulerException; +import org.quartz.Trigger; + +/** + * Unit tests for {@link QuartzScheduler}. + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class QuartzSchedulerTest { + + @Mock + private Injector injector; + + @Mock + private org.quartz.Scheduler quartzScheduler; + + private QuartzScheduler scheduler; + + @Before + public void setUp() + { + scheduler = new QuartzScheduler(injector, quartzScheduler); + } + + /** + * Tests {@link QuartzScheduler#schedule(java.lang.String, java.lang.Runnable)}. + * + * @throws SchedulerException + */ + @Test + public void testSchedule() throws SchedulerException + { + DummyRunnable dr = new DummyRunnable(); + Task task = scheduler.schedule("42 2 * * * ?", dr); + assertNotNull(task); + + ArgumentCaptor detailCaptor = ArgumentCaptor.forClass(JobDetail.class); + ArgumentCaptor triggerCaptor = ArgumentCaptor.forClass(Trigger.class); + verify(quartzScheduler).scheduleJob(detailCaptor.capture(), triggerCaptor.capture()); + + Trigger trigger = triggerCaptor.getValue(); + assertThat(trigger, is(instanceOf(CronTrigger.class))); + CronTrigger cron = (CronTrigger) trigger; + assertEquals("42 2 * * * ?", cron.getCronExpression()); + + JobDetail detail = detailCaptor.getValue(); + assertEquals(InjectionEnabledJob.class, detail.getJobClass()); + Provider runnable = (Provider) detail.getJobDataMap().get(Runnable.class.getName()); + assertNotNull(runnable); + assertSame(dr, runnable.get()); + assertEquals(injector, detail.getJobDataMap().get(Injector.class.getName())); + } + + /** + * Tests {@link QuartzScheduler#schedule(java.lang.String, java.lang.Class)}. + * + * @throws SchedulerException + */ + @Test + public void testScheduleWithClass() throws SchedulerException + { + scheduler.schedule("42 * * * * ?", DummyRunnable.class); + + verify(injector).getProvider(DummyRunnable.class); + + ArgumentCaptor detailCaptor = ArgumentCaptor.forClass(JobDetail.class); + ArgumentCaptor triggerCaptor = ArgumentCaptor.forClass(Trigger.class); + verify(quartzScheduler).scheduleJob(detailCaptor.capture(), triggerCaptor.capture()); + + Trigger trigger = triggerCaptor.getValue(); + assertThat(trigger, is(instanceOf(CronTrigger.class))); + CronTrigger cron = (CronTrigger) trigger; + assertEquals("42 * * * * ?", cron.getCronExpression()); + + JobDetail detail = detailCaptor.getValue(); + assertEquals(InjectionEnabledJob.class, detail.getJobClass()); + assertEquals(injector, detail.getJobDataMap().get(Injector.class.getName())); + } + + /** + * Tests {@link QuartzScheduler#init(sonia.scm.SCMContextProvider)}. + * + * @throws SchedulerException + */ + @Test + public void testInit() throws SchedulerException + { + when(quartzScheduler.isStarted()).thenReturn(Boolean.FALSE); + scheduler.init(null); + verify(quartzScheduler).start(); + } + + /** + * Tests {@link QuartzScheduler#init(sonia.scm.SCMContextProvider)} when the underlying scheduler is already started. + * + * @throws SchedulerException + */ + @Test + public void testInitAlreadyRunning() throws SchedulerException + { + when(quartzScheduler.isStarted()).thenReturn(Boolean.TRUE); + scheduler.init(null); + verify(quartzScheduler, never()).start(); + } + + /** + * Tests {@link QuartzScheduler#init(sonia.scm.SCMContextProvider)} when the underlying scheduler throws an exception. + * + * @throws SchedulerException + */ + @Test + public void testInitException() throws SchedulerException + { + when(quartzScheduler.isStarted()).thenThrow(SchedulerException.class); + scheduler.init(null); + verify(quartzScheduler, never()).start(); + } + + /** + * Tests {@link QuartzScheduler#close()}. + * + * @throws IOException + * @throws SchedulerException + */ + @Test + public void testClose() throws IOException, SchedulerException + { + when(quartzScheduler.isStarted()).thenReturn(Boolean.TRUE); + scheduler.close(); + verify(quartzScheduler).shutdown(); + } + + /** + * Tests {@link QuartzScheduler#close()} when the underlying scheduler is not running. + * + * @throws IOException + * @throws SchedulerException + */ + @Test + public void testCloseNotRunning() throws IOException, SchedulerException + { + when(quartzScheduler.isStarted()).thenReturn(Boolean.FALSE); + scheduler.close(); + verify(quartzScheduler, never()).shutdown(); + } + + /** + * Tests {@link QuartzScheduler#close()} when the underlying scheduler throws an exception. + * + * @throws IOException + * @throws SchedulerException + */ + @Test + public void testCloseException() throws IOException, SchedulerException + { + when(quartzScheduler.isStarted()).thenThrow(SchedulerException.class); + scheduler.close(); + verify(quartzScheduler, never()).shutdown(); + } + + + public static class DummyRunnable implements Runnable { + + @Override + public void run() + { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + } +} \ No newline at end of file diff --git a/scm-webapp/src/test/java/sonia/scm/schedule/QuartzTaskTest.java b/scm-webapp/src/test/java/sonia/scm/schedule/QuartzTaskTest.java new file mode 100644 index 0000000000..5ba5c19373 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/schedule/QuartzTaskTest.java @@ -0,0 +1,89 @@ +/*** + * Copyright (c) 2015, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * https://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.schedule; + +import org.junit.Test; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; +import static org.hamcrest.Matchers.*; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.quartz.JobKey; +import org.quartz.SchedulerException; + +/** + * Unit tests for {@link QuartzTask}. + * + * @author Sebastian Sdorra + */ +@RunWith(MockitoJUnitRunner.class) +public class QuartzTaskTest { + + @Mock + private org.quartz.Scheduler scheduler; + + private final JobKey jobKey = new JobKey("sample"); + + private QuartzTask task; + + @Before + public void setUp(){ + task = new QuartzTask(scheduler, jobKey); + } + + /** + * Tests {@link QuartzTask#cancel()}. + * + * @throws SchedulerException + */ + @Test + public void testCancel() throws SchedulerException + { + task.cancel(); + verify(scheduler).deleteJob(jobKey); + } + + /** + * Tests {@link QuartzTask#cancel()} when the scheduler throws an exception. + * @throws org.quartz.SchedulerException + */ + @Test(expected = RuntimeException.class) + public void testCancelWithException() throws SchedulerException + { + when(scheduler.deleteJob(jobKey)).thenThrow(SchedulerException.class); + task.cancel(); + } + +} \ No newline at end of file