diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a87e394 --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +# Project +.settings/ +*.iml +.idea/ + +# Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar + +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compilation folders +obj/ +bin/ + +#MacOS Files +.DS_Store + +#CLion Files +.idea/ + +# Visual code +.vscode + +# CMake +*cmake-build-*/ +*CMakeLists.txt + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f199ea1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 William Niemiec + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b7a6702 --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +![](https://github.com/wniemiec-task-java/scheduler/blob/master/docs/img/logo/logo.jpg) + +

Scheduler

+

Schedule routines to run after a certain time or whenever the the timer expires.

+

+ + + + Coverage status + Java compatibility + Maven Central release + License +

+
+ +## ❇ Introduction +Scheduler allows you to perform operations with routines so that they are executed according to a criterion. + +## ❓ How to use +1. Add one of the options below to the pom.xml file: + +#### Using Maven Central (recomended): +``` + + io.github.wniemiec-task-java + scheduler + LATEST + +``` + +#### Using GitHub Packages: +``` + + wniemiec.task.java + scheduler + LATEST + +``` + +2. Run +``` +$ mvn install +``` + +3. Use it +``` +[...] + +import wniemiec.task.java.Scheduler; + +[...] + +Scheduler.setTimeout(() -> { System.out.println("Hello..."); }, 1000); +Scheduler.setTimeout(() -> { System.out.println("World!"); }, 1000); +``` + +## 📖 Documentation +| Property |Parameter type|Return type|Description|Default parameter value| +|----------------|-------------------------------|-----------------------------|--------| +|setTimeout |`routine: Routine, delay: long`|`long`|Sets a timer which executes a routine once the timer expires| - | +|setInterval |`routine: Routine, delay: long`|`long`|Repeatedly calls a routine with a fixed time delay between each call| - | +|clearInterval |`id: long`|`void`|Cancels a timed, repeating action| - | +|clearTimeout |`id: long`|`void`|Cancels a timed action| - | +|clearAllTimeout | `void`|`void`|Clear all timeouts| - | +|clearAllIntervals | `void`|`void`|Clear all intervals| - | +|setTimeoutToRoutine|`routine: Routine, delay: long`|`long`|Runs a routine within a timeout. If the routine does not end on time, an interrupt signal will be sent to it| - | + +## 🚩 Changelog +Details about each version are documented in the [releases section](https://github.com/williamniemiec/wniemiec-task-java/scheduler/releases). + +## 🤝 Contribute! +See the documentation on how you can contribute to the project [here](https://github.com/wniemiec-task-java/scheduler/blob/master/CONTRIBUTING.md). + +## 📁 Files + +### / +| Name |Type|Description| +|----------------|-------------------------------|-----------------------------| +|dist |`Directory`|Released versions| +|docs |`Directory`|Documentation files| +|src |`Directory`| Source files| diff --git a/dist/1.x/scheduler-1.0.0.jar b/dist/1.x/scheduler-1.0.0.jar new file mode 100644 index 0000000..56bd63e Binary files /dev/null and b/dist/1.x/scheduler-1.0.0.jar differ diff --git a/pom-mavencentral.xml b/pom-mavencentral.xml new file mode 100644 index 0000000..0e2e02d --- /dev/null +++ b/pom-mavencentral.xml @@ -0,0 +1,212 @@ + + + + 4.0.0 + + io.github.wniemiec-task-java + scheduler + 1.0.0 + + Checkpoint + Schedule routines to run after a certain time or whenever the the timer expires. + https://github.com/wniemiec-task-java/scheduler + + + 1 + UTF-8 + 11 + + + + + MIT License + http://www.opensource.org/licenses/mit-license.php + + + + + + williamniemiec + William Niemiec + williamniemiec@hotmail.com + + + + + scm:git:git@github.com:wniemiec-task-java/scheduler.git + scm:git:ssh://github.com:wniemiec-task-java/scheduler.git + https://github.com/wniemiec-task-java/scheduler/tree/main + + + + + ossrh + https://s01.oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://s01.oss.sonatype.org/service/local/staging/deploy/maven2 + + + + + + junit + junit + 4.13.2 + test + + + + + + + src/main/java + + **/*.java + **/*.gwt.xml + + + + + + + maven-clean-plugin + 3.1.0 + + + + maven-resources-plugin + 3.0.2 + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + ${java.source-target.version} + ${java.source-target.version} + false + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + + + maven-jar-plugin + 3.0.2 + + ${project.build.directory}/../dist/${version.major}.x + + + + + maven-install-plugin + 2.5.2 + + + + maven-deploy-plugin + 2.8.2 + + + + maven-site-plugin + 3.7.1 + + + + maven-project-info-reports-plugin + 3.0.0 + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar-no-fork + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.2.0 + + + attach-javadocs + + jar + + + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.6 + + + jacoco-initialize + + prepare-agent + + + + jacoco-site + package + + report + + + + + + + + + + ci-cd + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + sign-artifacts + verify + + sign + + + + + --pinentry-mode + loopback + + + + + + + + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..c9c035d --- /dev/null +++ b/pom.xml @@ -0,0 +1,209 @@ + + + + 4.0.0 + + wniemiec.task.java + scheduler + 1.0.0 + + Scheduler + Schedule routines to run after a certain time or whenever the the timer expires. + https://github.com/wniemiec-task-java/scheduler + + + 1 + UTF-8 + 11 + + + + + MIT License + http://www.opensource.org/licenses/mit-license.php + + + + + + williamniemiec + William Niemiec + williamniemiec@hotmail.com + + + + + scm:git:git@github.com:wniemiec-task-java/scheduler.git + scm:git:ssh://github.com:wniemiec-task-java/scheduler.git + https://github.com/wniemiec-task-java/scheduler/tree/main + + + + + github-wniemiec-task-java + GitHub wniemiec-task-java Apache Maven Packages + https://maven.pkg.github.com/wniemiec-task-java/scheduler + + + + + + junit + junit + 4.13.2 + test + + + + + + + src/main/java + + **/*.java + **/*.gwt.xml + + + + + + + maven-clean-plugin + 3.1.0 + + + + maven-resources-plugin + 3.0.2 + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + ${java.source-target.version} + ${java.source-target.version} + false + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + + + maven-jar-plugin + 3.0.2 + + ${project.build.directory}/../dist/${version.major}.x + + + + + maven-install-plugin + 2.5.2 + + + + maven-deploy-plugin + 2.8.2 + + + + maven-site-plugin + 3.7.1 + + + + maven-project-info-reports-plugin + 3.0.0 + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar-no-fork + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.2.0 + + + attach-javadocs + + jar + + + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.6 + + + jacoco-initialize + + prepare-agent + + + + jacoco-site + package + + report + + + + + + + + + + ci-cd + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + sign-artifacts + verify + + sign + + + + + --pinentry-mode + loopback + + + + + + + + + + diff --git a/src/main/java/wniemiec/task/java/Scheduler.java b/src/main/java/wniemiec/task/java/Scheduler.java new file mode 100644 index 0000000..f49c3a0 --- /dev/null +++ b/src/main/java/wniemiec/task/java/Scheduler.java @@ -0,0 +1,268 @@ +/** + * Copyright (c) William Niemiec. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package wniemiec.task.java; + +import java.util.*; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; + +/** + * Responsible for calling routines after a certain time or whenever the the + * timer expires. + */ +public class Scheduler { + + //------------------------------------------------------------------------- + // Attributes + //------------------------------------------------------------------------- + /** + * Stores routines sent to setInterval method along with its ids. + */ + private static final Map intervalRoutines; + + /** + * Stores routines sent to setTimeout method along with its ids. + */ + private static final Map delayRoutines; + + private static final Map timeoutRoutines; + private static final Map timeoutRoutineThreads; + private static Long currentRoutineId; + + + //------------------------------------------------------------------------- + // Initialization blocks + //------------------------------------------------------------------------- + static { + intervalRoutines = new HashMap(); + delayRoutines = new HashMap(); + timeoutRoutines = new HashMap<>(); + timeoutRoutineThreads = new HashMap<>(); + } + + + //------------------------------------------------------------------------- + // Constructor + //------------------------------------------------------------------------- + private Scheduler() { + } + + + //------------------------------------------------------------------------- + // Methods + //------------------------------------------------------------------------- + /** + * Sets a timer which executes a routine once the timer expires. + * + * @param routine Routine to be performed + * @param delay Waiting time before the routine is executed (in + * milliseconds) + * + * @return Routine identifier (necessary to be able to stop it) + * + * @throws IllegalArgumentException If routine is null + */ + public static long setTimeout(Runnable routine, long delay) { + if (routine == null) + throw new IllegalArgumentException("Routine cannot be null"); + + if (delay < 0) + throw new IllegalArgumentException("Delay cannot be negative"); + + initializeRoutineId(); + scheduleTimeout(routine, delay); + + return currentRoutineId; + } + + private static void initializeRoutineId() { + currentRoutineId = getCurrentTime(); + } + + private static void scheduleTimeout(Runnable routine, long delay) { + if (currentRoutineId == null) + throw new IllegalStateException("Routine id was not initialized"); + + Timer timer = new Timer(); + timer.schedule(createTaskFromRoutine(routine), delay); + + delayRoutines.put(currentRoutineId, timer); + } + + private static TimerTask createTaskFromRoutine(Runnable routine) { + return new TimerTask() { + @Override + public void run() { + routine.run(); + } + }; + } + + /** + * Repeatedly calls a routine with a fixed time delay between each call. + * + * @param routine Routine to be performed + * @param interval Interval that the routine will be invoked (in + * milliseconds) + * + * @return Routine identifier (necessary to be able to stop it) + * + * @throws IllegalArgumentException If routine is null + */ + public static long setInterval(Runnable routine, long interval) { + if (routine == null) + throw new IllegalArgumentException("Routine cannot be null"); + + if (interval < 0) + throw new IllegalArgumentException("Interval cannot be negative"); + + initializeRoutineId(); + scheduleInterval(routine, interval); + + return currentRoutineId; + } + + private static void scheduleInterval(Runnable routine, long interval) { + if (currentRoutineId == null) + throw new IllegalStateException("Routine id was not initialized"); + + Timer timer = new Timer(); + timer.scheduleAtFixedRate(createTaskFromRoutine(routine), 0, interval); + + intervalRoutines.put(currentRoutineId, timer); + } + + /** + * Cancels a timed, repeating action, which was previously established by a + * call to {@link #setInterval(Runnable, long)}. + * + * @param id Routine identifier + */ + public static void clearInterval(long id) { + if (!intervalRoutines.containsKey(id)) + return; + + intervalRoutines.get(id).cancel(); + intervalRoutines.remove(id); + } + + /** + * Cancels a timed action which was previously established by a + * call to {@link #setTimeout(Runnable, long)}. + * + * @param id Routine identifier + */ + public static void clearTimeout(long id) { + if (!delayRoutines.containsKey(id)) + return; + + delayRoutines.get(id).cancel(); + delayRoutines.remove(id); + } + + public static void clearAllTimeout() { + for (Long timeoutId : delayRoutines.keySet()) { + clearTimeout(timeoutId); + } + } + + public static void clearAllInterval() { + for (Long intervalId : intervalRoutines.keySet()) { + clearInterval(intervalId); + } + } + + /** + * Runs a routine within a timeout. If the routine does not end on time, an + * interrupt signal will be sent to it. + * + * @param routine Routine + * @param timeout Maximum execution time (in milliseconds) + * + * @return True if the routine has not finished executing within the + * time limit; false otherwise + * + * @throws IllegalArgumentException If routine is null or if timeout is + * is negative + */ + public static boolean setTimeoutToRoutine(Runnable routine, long timeout) { + if (routine == null) + throw new IllegalArgumentException("Routine cannot be null"); + + if (timeout < 0) + throw new IllegalArgumentException("Timeout cannot be negative"); + + runRoutine(routine); + waitRoutineFor(timeout); + finishRoutine(); + + return !hasRoutineFinished(); + } + + private static long getCurrentTime() { + return new Date().getTime(); + } + + private static void runRoutine(Runnable routine) { + initializeRoutineId(); + timeoutRoutines.put(currentRoutineId, false); + + ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); + Future handler = executor.submit(() -> { + routine.run(); + + if (timeoutRoutines.containsKey(currentRoutineId)) { + timeoutRoutines.put(currentRoutineId, true); + timeoutRoutineThreads.remove(currentRoutineId); + } + }); + executor.shutdown(); + + timeoutRoutineThreads.put(currentRoutineId, handler); + } + + private static void waitRoutineFor(long time) { + if (currentRoutineId < 0) + return; + + long start = getCurrentTime(); + + while ((timeElapsedInMilliseconds(start) < time) && !hasRoutineFinished()) { + try { + Thread.sleep(200); + } + catch (InterruptedException e) { + break; + } + } + } + + private static long timeElapsedInMilliseconds(long start) { + return (getCurrentTime() - start); + } + + private static boolean hasRoutineFinished() { + if (currentRoutineId < 0) + return true; + + if (!timeoutRoutines.containsKey(currentRoutineId)) + return false; + + return timeoutRoutines.get(currentRoutineId); + } + + private static void finishRoutine() { + if (hasRoutineFinished()) + return; + + timeoutRoutines.remove(currentRoutineId); + timeoutRoutineThreads.get(currentRoutineId).cancel(true); + timeoutRoutineThreads.remove(currentRoutineId); + } +} diff --git a/src/test/java/wniemiec/task/java/SchedulerTest.java b/src/test/java/wniemiec/task/java/SchedulerTest.java new file mode 100644 index 0000000..213bb61 --- /dev/null +++ b/src/test/java/wniemiec/task/java/SchedulerTest.java @@ -0,0 +1,158 @@ +package wniemiec.task.java; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; + +public class SchedulerTest { + + //------------------------------------------------------------------------- + // Attributes + //------------------------------------------------------------------------- + private static final long DELAY_TIME; + private static final long INTERVAL_TIME; + private static final long MAX_WAIT_TIME; + private volatile boolean insideRoutine; + private int totInsideRoutine; + + + //------------------------------------------------------------------------- + // Initialization blocks + //------------------------------------------------------------------------- + static { + DELAY_TIME = 1000L; + INTERVAL_TIME = 100L; + MAX_WAIT_TIME = 1000L; + } + + + //------------------------------------------------------------------------- + // Test hooks + //------------------------------------------------------------------------- + @Before + public void beforeEachTest() { + insideRoutine = false; + totInsideRoutine = 0; + Scheduler.clearAllTimeout(); + Scheduler.clearAllInterval(); + } + + + //------------------------------------------------------------------------- + // Tests + //------------------------------------------------------------------------- + @Test + public void testTimeout() throws InterruptedException { + Scheduler.setTimeout(() -> { + insideRoutine = true; + }, DELAY_TIME); + + waitForTimeout(); + + assertTrue(insideRoutine); + } + + @Test + public void testInterval() throws InterruptedException { + totInsideRoutine = 0; + + long scheduleId = Scheduler.setInterval(() -> { + if (totInsideRoutine == 3) + return; + + totInsideRoutine++; + }, INTERVAL_TIME); + + waitForInterval(3); + + if (totInsideRoutine == 3) + Scheduler.clearInterval(scheduleId); + + assertTrue(totInsideRoutine == 3); + } + + @Test + public void testClearTimeout() throws InterruptedException { + long scheduleId = Scheduler.setTimeout(() -> { + insideRoutine = true; + }, DELAY_TIME); + + Scheduler.clearTimeout(scheduleId); + + waitForTimeout(); + + assertFalse(insideRoutine); + } + + @Test + public void testClearInterval() throws InterruptedException { + totInsideRoutine = 0; + + long scheduleId = Scheduler.setInterval(() -> { + totInsideRoutine++; + }, INTERVAL_TIME); + + Scheduler.clearInterval(scheduleId); + + waitForInterval(2); + + assertTrue(totInsideRoutine == 0); + } + + @Test(expected = IllegalArgumentException.class) + public void testTimeoutWithNullRoutine() { + Scheduler.setTimeout(null, DELAY_TIME); + } + + @Test(expected = IllegalArgumentException.class) + public void testIntervalWithNullRoutine() { + Scheduler.setInterval(null, INTERVAL_TIME); + } + + @Test(expected = IllegalArgumentException.class) + public void testTimeoutWithNegativeDelay() { + Scheduler.setTimeout(() -> {}, -1); + } + + @Test(expected = IllegalArgumentException.class) + public void testIntervalWithNegativeDelay() { + Scheduler.setInterval(() -> {}, -1); + } + + @Test + public void testSetTimeoutToRoutine() { + boolean timeout = Scheduler.setTimeoutToRoutine(() -> { + while (true) + ; + }, MAX_WAIT_TIME); + + assertTrue(timeout); + } + + @Test + public void testSetTimeoutToRoutine2() { + boolean timeout = Scheduler.setTimeoutToRoutine(() -> { + int x = 1; + + while (x >= 0) + x--; + }, 99000); + + assertFalse(timeout); + } + + + + //------------------------------------------------------------------------- + // Methods + //-------------------------------------------------------------------------\ + private void waitForInterval(int totalExecutions) throws InterruptedException { + Thread.sleep((INTERVAL_TIME * totalExecutions) + 100); + } + + private void waitForTimeout() throws InterruptedException { + Thread.sleep(DELAY_TIME + 100); + } +}