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 extends Runnable> 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 extends Runnable> runnable)
+ {
+ return schedule(expression, injector.getProvider(runnable));
+ }
+
+ private Task schedule(String expression, Provider extends Runnable> 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