add cron expressions, update to OpenJDK 11, gradle 5.6

This commit is contained in:
Jörg Prante 2019-09-10 14:52:43 +02:00
parent 74d51599b1
commit eb0b999f4e
79 changed files with 5286 additions and 1422 deletions

View file

@ -7,6 +7,8 @@ org.xbib.time is based upon the following software:
- prettytime https://github.com/ocpsoft/prettytime (Apache 2.0)
- cron expression https://github.com/anderswisch/cron-expression/ (MIT License)
with improvements by Jörg Prante including
- converted to Java 8 java.time API
@ -16,3 +18,5 @@ with improvements by Jörg Prante including
- refactoring classes
- rewritten code to simplify and ease development
- added nextExecution() method to cron expression and scheduling via Callable

View file

@ -1,76 +1,134 @@
plugins {
id "org.sonarqube" version "2.2"
id 'org.ajoberstar.github-pages' version '1.6.0-rc.1'
id "org.xbib.gradle.plugin.jbake" version "1.2.1"
id "com.github.spotbugs" version "2.0.0"
id "io.codearte.nexus-staging" version "0.11.0"
}
printf "Host: %s\nOS: %s %s %s\nJVM: %s %s %s %s\nGroovy: %s\nGradle: %s\n" +
"Build: group: ${project.group} name: ${project.name} version: ${project.version}\n",
InetAddress.getLocalHost(),
System.getProperty("os.name"),
System.getProperty("os.arch"),
System.getProperty("os.version"),
System.getProperty("java.version"),
System.getProperty("java.vm.version"),
System.getProperty("java.vm.vendor"),
System.getProperty("java.vm.name"),
GroovySystem.getVersion(),
gradle.gradleVersion
apply plugin: 'java'
apply plugin: 'maven'
apply plugin: 'signing'
apply plugin: 'findbugs'
apply plugin: 'pmd'
apply plugin: 'checkstyle'
apply plugin: "jacoco"
apply plugin: 'org.ajoberstar.github-pages'
configurations {
wagon
provided
testCompile.extendsFrom(provided)
}
apply plugin: "com.github.spotbugs"
dependencies {
testCompile 'junit:junit:4.12'
testCompile 'org.apache.logging.log4j:log4j-core:2.7'
testCompile 'org.apache.logging.log4j:log4j-jul:2.7'
wagon 'org.apache.maven.wagon:wagon-ssh-external:2.10'
testCompile "junit:junit:${project.property('junit.version')}"
testCompile "org.quartz-scheduler:quartz:${project.property('quartz.version')}"
testCompile "com.google.caliper:caliper:${project.property('caliper.version')}"
}
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
compileJava {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
compileTestJava {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
[compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
tasks.withType(JavaCompile) {
options.compilerArgs << "-Xlint:all" << "-profile" << "compact2"
options.compilerArgs << "-Xlint:all"
}
test {
testLogging {
showStandardStreams = false
showStandardStreams = true
exceptionFormat = 'full'
}
systemProperty 'java.util.logging.manager', 'org.apache.logging.log4j.jul.LogManager'
}
spotbugs {
toolVersion = '3.1.12'
sourceSets = [sourceSets.main]
ignoreFailures = true
effort = "max"
reportLevel = "high"
}
tasks.withType(Pmd) {
ignoreFailures = true
reports {
xml.enabled = true
html.enabled = true
}
}
tasks.withType(Checkstyle) {
ignoreFailures = true
reports {
xml.enabled = true
html.enabled = true
}
}
task sourcesJar(type: Jar, dependsOn: classes) {
classifier 'sources'
from sourceSets.main.allSource
}
task javadocJar(type: Jar, dependsOn: javadoc) {
classifier 'javadoc'
}
artifacts {
archives sourcesJar, javadocJar
}
if (project.hasProperty('signing.keyId')) {
signing {
sign configurations.archives
ext {
user = 'xbib'
projectName = 'time'
projectDescription = 'A bundle of Chronic, Prettytime, and org.joda.time.format optimized for Java 8 Time API'
scmUrl = 'https://github.com/xbib/time'
scmConnection = 'scm:git:git://github.com/xbib/time.git'
scmDeveloperConnection = 'scm:git:git://github.com/xbib/time.git'
}
task sonatypeUpload(type: Upload) {
configuration = configurations.archives
uploadDescriptor = true
repositories {
if (project.hasProperty('ossrhUsername')) {
mavenDeployer {
beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
repository(url: uri(ossrhReleaseUrl)) {
authentication(userName: ossrhUsername, password: ossrhPassword)
}
snapshotRepository(url: uri(ossrhSnapshotUrl)) {
authentication(userName: ossrhUsername, password: ossrhPassword)
}
pom.project {
name projectName
description projectDescription
packaging 'jar'
inceptionYear '2016'
url scmUrl
organization {
name 'xbib'
url 'http://xbib.org'
}
developers {
developer {
id user
name 'Jörg Prante'
email 'joergprante@gmail.com'
url 'https://github.com/jprante'
}
}
scm {
url scmUrl
connection scmConnection
developerConnection scmDeveloperConnection
}
licenses {
license {
name 'The Apache License, Version 2.0'
url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
}
}
}
}
}
apply from: 'gradle/ext.gradle'
apply from: 'gradle/publish.gradle'
apply from: 'gradle/sonarqube.gradle'
nexusStaging {
packageGroup = "org.xbib"
}

View file

@ -1,3 +1,8 @@
group = org.xbib
name = time
version = 1.0.0
version = 2.0.0
# test
junit.version = 4.12
quartz.version = 2.3.0
caliper.version = 1.0-beta-2

View file

@ -1,8 +0,0 @@
ext {
user = 'xbib'
projectName = 'time'
projectDescription = 'A bundle of Chronic, Prettytime, and org.joda.time.format optimized for Java 8 Time API'
scmUrl = 'https://github.com/xbib/time'
scmConnection = 'scm:git:git://github.com/xbib/time.git'
scmDeveloperConnection = 'scm:git:git://github.com/xbib/time.git'
}

View file

@ -1,63 +0,0 @@
task xbibUpload(type: Upload) {
configuration = configurations.archives
uploadDescriptor = true
repositories {
if (project.hasProperty('xbibUsername')) {
mavenDeployer {
configuration = configurations.wagon
repository(url: uri('scpexe://xbib.org/repository')) {
authentication(userName: xbibUsername, privateKey: xbibPrivateKey)
}
}
}
}
}
task sonatypeUpload(type: Upload) {
configuration = configurations.archives
uploadDescriptor = true
repositories {
if (project.hasProperty('ossrhUsername')) {
mavenDeployer {
beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
repository(url: uri(ossrhReleaseUrl)) {
authentication(userName: ossrhUsername, password: ossrhPassword)
}
snapshotRepository(url: uri(ossrhSnapshotUrl)) {
authentication(userName: ossrhUsername, password: ossrhPassword)
}
pom.project {
name projectName
description projectDescription
packaging 'jar'
inceptionYear '2016'
url scmUrl
organization {
name 'xbib'
url 'http://xbib.org'
}
developers {
developer {
id user
name 'Jörg Prante'
email 'joergprante@gmail.com'
url 'https://github.com/jprante'
}
}
scm {
url scmUrl
connection scmConnection
developerConnection scmDeveloperConnection
}
licenses {
license {
name 'The Apache License, Version 2.0'
url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
}
}
}
}
}

View file

@ -1,36 +0,0 @@
tasks.withType(FindBugs) {
ignoreFailures = true
reports {
xml.enabled = true
}
}
tasks.withType(Pmd) {
ignoreFailures = true
reports {
xml.enabled = true
}
}
tasks.withType(Checkstyle) {
ignoreFailures = true
reports {
xml.enabled = true
}
}
jacocoTestReport {
reports {
xml.enabled true
xml.destination "${buildDir}/reports/jacoco-xml"
}
}
sonarqube {
properties {
property "sonar.projectName", "${project.group} ${project.name}"
property "sonar.sourceEncoding", "UTF-8"
property "sonar.tests", "src/test/java"
property "sonar.scm.provider", "git"
property "sonar.java.coveragePlugin", "jacoco"
property "sonar.junit.reportsPath", "build/test-results/test/"
}
}

Binary file not shown.

View file

@ -1,6 +1,6 @@
#Wed Nov 30 14:21:32 CET 2016
#Mon Sep 09 15:32:28 CEST 2019
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6-all.zip
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-3.2.1-all.zip
zipStoreBase=GRADLE_USER_HOME

22
gradlew vendored
View file

@ -1,5 +1,21 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
@ -28,7 +44,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
@ -109,8 +125,8 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`

18
gradlew.bat vendored
View file

@ -1,3 +1,19 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

View file

@ -33,39 +33,39 @@ public class Chronic {
/**
* Parses a string containing a natural language date or time. If the parser
* can find a date or time, either a Time or Chronic::Span will be returned
* (depending on the value of <tt>:guess</tt>). If no date or time can be found,
* (depending on the value of {@code :guess}). If no date or time can be found,
* +nil+ will be returned.
* <p>
* Options are:
* <p>
* [<tt>:context</tt>]
* <tt>:past</tt> or <tt>:future</tt> (defaults to <tt>:future</tt>)
* [{@code :context}]
* {@code :past} or {@code :future} (defaults to {@code :future})
* <p>
* If your string represents a birthday, you can set <tt>:context</tt> to <tt>:past</tt>
* If your string represents a birthday, you can set {@code :context} to {@code :past}
* and if an ambiguous string is given, it will assume it is in the
* past. Specify <tt>:future</tt> or omit to set a future context.
* past. Specify {@code :future<} or omit to set a future context.
* <p>
* [<tt>:now</tt>]
* [{@code :now}]
* Time (defaults to Time.now)
* <p>
* By setting <tt>:now</tt> to a Time, all computations will be based off
* By setting {@code :now} to a Time, all computations will be based off
* of that time instead of Time.now
* <p>
* [<tt>:guess</tt>]
* [{@code :guess<}]
* +true+ or +false+ (defaults to +true+)
* <p>
* By default, the parser will guess a single point in time for the
* given date or time. If you'd rather have the entire time span returned,
* set <tt>:guess</tt> to +false+ and a Chronic::Span will be returned.
* set {@code :guess} to +false+ and a Chronic::Span will be returned.
* <p>
* [<tt>:ambiguous_time_range</tt>]
* Integer or <tt>:none</tt> (defaults to <tt>6</tt> (6am-6pm))
* [{@code :ambiguous_time_range}]
* Integer or {@code :none} (defaults to {@code 6} (6am-6pm))
* <p>
* If an Integer is given, ambiguous times (like 5:00) will be
* assumed to be within the range of that time in the AM to that time
* in the PM. For example, if you set it to <tt>7</tt>, then the parser will
* in the PM. For example, if you set it to {@code 7}, then the parser will
* look for the time between 7am and 7pm. In the case of 5:00, it would
* assume that means 5:00pm. If <tt>:none</tt> is given, no assumption
* assume that means 5:00pm. If {@code :none} is given, no assumption
* will be made, and the first matching instance of that time will
* be used.
* @param text text

View file

@ -43,7 +43,9 @@ public abstract class RepeaterUnit extends Repeater<Object> {
String unitName = unitNameEnum.name();
String capitalizedUnitName = unitName.substring(0, 1) + unitName.substring(1).toLowerCase();
String repeaterClassName = RepeaterUnit.class.getPackage().getName() + ".Repeater" + capitalizedUnitName;
return Class.forName(repeaterClassName).asSubclass(RepeaterUnit.class).newInstance();
return Class.forName(repeaterClassName)
.asSubclass(RepeaterUnit.class)
.getConstructor().newInstance();
}
}
return null;

View file

@ -0,0 +1,130 @@
package org.xbib.time.schedule;
import java.time.ZonedDateTime;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public abstract class CronExpression {
public abstract boolean matches(ZonedDateTime t);
public abstract ZonedDateTime nextExecution(ZonedDateTime from, ZonedDateTime to);
private static final String YEARLY = "0 0 1 1 *",
MONTHLY = "0 0 1 * *",
WEEKLY = "0 0 * * 7",
DAILY = "0 0 * * *",
HOURLY = "0 * * * *";
private static final Map<String, String> ALIASES = Map.ofEntries(
Map.entry("yearly", YEARLY),
Map.entry("annually", YEARLY),
Map.entry("monthly", MONTHLY),
Map.entry("weekly", WEEKLY),
Map.entry("daily", DAILY),
Map.entry("midnight", DAILY),
Map.entry("hourly", HOURLY));
private static final Pattern ALIAS_PATTERN = Pattern.compile("[a-z]+");
private static final boolean DEFAULT_ONE_BASED_DAY_OF_WEEK = false;
private static final boolean DEFAULT_SECONDS = false;
private static final boolean DEFAULT_ALLOW_BOTH_DAYS = true;
public static CronExpression yearly() {
return parse(YEARLY);
}
public static CronExpression monthly() {
return parse(MONTHLY);
}
public static CronExpression weekly() {
return parse(WEEKLY);
}
public static CronExpression daily() {
return parse(DAILY);
}
public static CronExpression hourly() {
return parse(HOURLY);
}
public static boolean isValid(String s) {
return isValid(s, DEFAULT_ONE_BASED_DAY_OF_WEEK, DEFAULT_SECONDS, DEFAULT_ALLOW_BOTH_DAYS);
}
public static CronExpression parse(String s) {
return parse(s, DEFAULT_ONE_BASED_DAY_OF_WEEK, DEFAULT_SECONDS, DEFAULT_ALLOW_BOTH_DAYS);
}
private static boolean isValid(String s, boolean oneBasedDayOfWeek, boolean seconds, boolean allowBothDays) {
boolean valid;
try {
parse(s, oneBasedDayOfWeek, seconds, allowBothDays);
valid = true;
} catch (Exception e) {
valid = false;
}
return valid;
}
private static CronExpression parse(String s, boolean oneBasedDayOfWeek, boolean seconds, boolean allowBothDays) {
Objects.requireNonNull(s);
if (s.charAt(0) == '@') {
Matcher aliasMatcher = ALIAS_PATTERN.matcher(s);
if (aliasMatcher.find(1)) {
String alias = aliasMatcher.group();
if (ALIASES.containsKey(alias)) {
return new DefaultCronExpression(ALIASES.get(alias),
DEFAULT_ONE_BASED_DAY_OF_WEEK, DEFAULT_SECONDS, DEFAULT_ALLOW_BOTH_DAYS);
} else if ("reboot".equals(alias)) {
return new RebootCronExpression();
}
}
}
return new DefaultCronExpression(s, seconds, oneBasedDayOfWeek, allowBothDays);
}
public static Parser parser() {
return new Parser();
}
public static class Parser {
private boolean oneBasedDayOfWeek, seconds, allowBothDays;
private Parser() {
oneBasedDayOfWeek = DEFAULT_ONE_BASED_DAY_OF_WEEK;
seconds = DEFAULT_SECONDS;
allowBothDays = DEFAULT_ALLOW_BOTH_DAYS;
}
public boolean isValid(String s) {
return CronExpression.isValid(s, oneBasedDayOfWeek, seconds, allowBothDays);
}
public CronExpression parse(String s) {
return CronExpression.parse(s, oneBasedDayOfWeek, seconds, allowBothDays);
}
public Parser withOneBasedDayOfWeek(boolean oneBasedDayOfWeek) {
this.oneBasedDayOfWeek = oneBasedDayOfWeek;
return this;
}
public Parser withSecondsField(boolean secondsField) {
this.seconds = secondsField;
return this;
}
public Parser allowBothDayFields(boolean allowBothDayFields) {
this.allowBothDays = allowBothDayFields;
return this;
}
}
}

View file

@ -0,0 +1,82 @@
package org.xbib.time.schedule;
import java.io.Closeable;
import java.io.IOException;
import java.time.Clock;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class CronSchedule<T> implements Closeable {
private final ScheduledExecutorService executor;
private final List<Entry<T>> entries;
private final int periodInMilliseconds;
private ScheduledFuture<?> future;
public CronSchedule(ScheduledExecutorService scheduledExecutorServices) {
this(scheduledExecutorServices, 60000);
}
public CronSchedule(ScheduledExecutorService scheduledExecutorServices,
int periodInMilliseconds) {
this.executor = scheduledExecutorServices;
this.entries = new ArrayList<>();
this.periodInMilliseconds = periodInMilliseconds;
}
public void add(String name, CronExpression expression, Callable<T> callable) {
entries.add(new Entry<T>(name, expression, callable));
}
public void remove(String name) {
entries.removeIf(entry -> name.equals(entry.getName()));
}
public void start() {
long initialDelay = periodInMilliseconds - (Clock.systemDefaultZone().millis() % periodInMilliseconds);
this.future = executor.scheduleAtFixedRate(CronSchedule.this::run,
initialDelay, periodInMilliseconds, TimeUnit.MILLISECONDS);
}
public void run() {
run(ZonedDateTime.now());
}
public void run(ZonedDateTime time) {
for (Entry<T> entry : entries) {
if (entry.getCronExpression().matches(time)) {
entry.setLastCalled(time);
executor.submit(entry.getCallable());
}
}
}
@Override
public void close() throws IOException {
if (future != null) {
future.cancel(true);
future = null;
}
if (executor != null) {
executor.shutdownNow();
try {
executor.awaitTermination(30, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new IOException(e);
}
}
}
@Override
public String toString() {
return entries.toString();
}
}

View file

@ -0,0 +1,122 @@
package org.xbib.time.schedule;
import java.time.DayOfWeek;
import java.time.ZonedDateTime;
import java.time.temporal.TemporalAdjusters;
public class DayOfMonthField extends DefaultField {
private final boolean lastDay;
private final boolean nearestWeekday;
private final boolean unspecified;
private DayOfMonthField(Builder b) {
super(b);
this.lastDay = b.lastDay;
this.nearestWeekday = b.nearestWeekday;
this.unspecified = b.unspecified;
}
boolean isUnspecified() {
return unspecified;
}
public boolean matches(ZonedDateTime time) {
if (unspecified) {
return true;
}
final int dayOfMonth = time.getDayOfMonth();
if (lastDay) {
return dayOfMonth == time.with(TemporalAdjusters.lastDayOfMonth()).getDayOfMonth();
} else if (nearestWeekday) {
DayOfWeek dayOfWeek = time.getDayOfWeek();
if ((dayOfWeek == DayOfWeek.MONDAY && contains(time.minusDays(1).getDayOfMonth()))
|| (dayOfWeek == DayOfWeek.FRIDAY && contains(time.plusDays(1).getDayOfMonth()))) {
return true;
}
}
return contains(dayOfMonth);
}
public static DayOfMonthField parse(Tokens s) {
return new Builder().parse(s).build();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
if (!super.equals(o)) {
return false;
}
DayOfMonthField that = (DayOfMonthField) o;
return lastDay == that.lastDay && nearestWeekday == that.nearestWeekday;
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + (lastDay ? 1 : 0);
result = 31 * result + (nearestWeekday ? 1 : 0);
return result;
}
@Override
public String toString() {
return isFullRange() ? "*" : getNumbers().toString();
}
public static class Builder extends DefaultField.Builder {
private boolean lastDay;
private boolean nearestWeekday;
private boolean unspecified;
Builder() {
super(1, 31);
}
@Override
public DayOfMonthField build() {
return new DayOfMonthField(this);
}
@Override
protected Builder parse(Tokens tokens) {
super.parse(tokens);
return this;
}
@Override
protected boolean parseValue(Tokens tokens, Token token, int first, int last) {
if (token == Token.MATCH_ONE) {
unspecified = true;
return false;
} else if (token == Token.LAST) {
lastDay = true;
return false;
} else {
return super.parseValue(tokens, token, first, last);
}
}
@Override
protected boolean parseNumber(Tokens tokens, Token token, int first, int last) {
if (token == Token.WEEKDAY) {
add(first);
nearestWeekday = true;
return false;
} else {
return super.parseNumber(tokens, token, first, last);
}
}
}
}

View file

@ -0,0 +1,173 @@
package org.xbib.time.schedule;
import org.xbib.time.util.LinkedHashSetMultiMap;
import org.xbib.time.util.MultiMap;
import java.time.DayOfWeek;
import java.time.ZonedDateTime;
import java.time.temporal.TemporalAdjusters;
import java.util.LinkedHashSet;
import java.util.Set;
public class DayOfWeekField extends DefaultField {
private final MultiMap<Integer, Integer> nth;
private final Set<Integer> last;
private final boolean hasNth;
private final boolean hasLast;
private final boolean unspecified;
private DayOfWeekField(Builder b) {
super(b);
this.nth = b.nth;
hasNth = !nth.isEmpty();
this.last = b.last;
hasLast = !last.isEmpty();
unspecified = b.unspecified;
}
public boolean isUnspecified() {
return unspecified;
}
public boolean matches(ZonedDateTime time) {
if (unspecified) {
return true;
}
final DayOfWeek dayOfWeek = time.getDayOfWeek();
//int number = dayOfWeek.getValue() % 7;
int number = dayOfWeek.getValue();
if (hasLast) {
return last.contains(number) && time.getMonth() != time.plusWeeks(1).getMonth();
} else if (hasNth) {
int dayOfYear = time.getDayOfYear();
if (nth.containsKey(number)) {
for (int possibleMatch : nth.get(number)) {
if (dayOfYear == time.with(TemporalAdjusters.dayOfWeekInMonth(possibleMatch, dayOfWeek)).getDayOfYear()) {
return true;
}
}
}
}
return contains(number);
}
private int number(int dayOfWeek) {
return dayOfWeek % 7;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
if (!super.equals(o)) {
return false;
}
DayOfWeekField that = (DayOfWeekField) o;
if (hasLast != that.hasLast) {
return false;
}
if (hasNth != that.hasNth) {
return false;
}
if (!last.equals(that.last)) {
return false;
}
return nth.equals(that.nth);
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + nth.hashCode();
result = 31 * result + last.hashCode();
result = 31 * result + (hasNth ? 1 : 0);
result = 31 * result + (hasLast ? 1 : 0);
return result;
}
public static DayOfWeekField parse(Tokens s, boolean oneBased) {
return new Builder(oneBased).parse(s).build();
}
public static class Builder extends DefaultField.Builder {
static final Keywords KEYWORDS = new Keywords();
static {
KEYWORDS.put("MON", 1);
KEYWORDS.put("TUE", 2);
KEYWORDS.put("WED", 3);
KEYWORDS.put("THU", 4);
KEYWORDS.put("FRI", 5);
KEYWORDS.put("SAT", 6);
KEYWORDS.put("SUN", 7);
}
private boolean oneBased;
private boolean unspecified;
private final Set<Integer> last;
private final MultiMap<Integer, Integer> nth;
Builder(boolean oneBased) {
super(1, 7);
this.oneBased = oneBased;
last = new LinkedHashSet<>();
nth = new LinkedHashSetMultiMap<>();
}
@Override
protected Builder parse(Tokens tokens) {
tokens.keywords(KEYWORDS);
if (oneBased) {
tokens.offset(1);
}
super.parse(tokens);
tokens.reset();
return this;
}
@Override
protected boolean parseValue(Tokens tokens, Token token, int first, int last) {
if (token == Token.MATCH_ONE) {
unspecified = true;
return false;
} else {
return super.parseValue(tokens, token, first, last);
}
}
@Override
protected boolean parseNumber(Tokens tokens, Token token, int first, int last) {
if (token == Token.LAST) {
this.last.add(first);
} else if (token == Token.NTH) {
int number = nextNumber(tokens);
if (oneBased) {
number += 1;
}
if (number == 0) {
number = 7;
}
nth.put(first, number);
} else {
return super.parseNumber(tokens, token, first, last);
}
return false;
}
@Override
public DayOfWeekField build() {
return new DayOfWeekField(this);
}
}
}

View file

@ -0,0 +1,228 @@
package org.xbib.time.schedule;
import java.time.DayOfWeek;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import java.util.Objects;
import java.util.SortedSet;
import java.util.logging.Logger;
public class DefaultCronExpression extends CronExpression {
private final String string;
private final TimeField second;
private final TimeField minute;
private final TimeField hour;
private final TimeField month;
private final TimeField year;
private final DayOfWeekField dayOfWeek;
private final DayOfMonthField dayOfMonth;
DefaultCronExpression(String string, boolean seconds, boolean oneBasedDayOfWeek, boolean allowBothDayFields) {
this.string = string;
if (string.isEmpty()) {
throw new IllegalArgumentException("empty spec not allowed");
}
String s = string.toUpperCase();
Tokens tokens = new Tokens(s);
if (seconds) {
second = DefaultField.parse(tokens, 0, 59);
} else {
second = MatchAllField.instance;
}
minute = DefaultField.parse(tokens, 0, 59);
hour = DefaultField.parse(tokens, 0, 23);
dayOfMonth = DayOfMonthField.parse(tokens);
month = MonthField.parse(tokens);
dayOfWeek = DayOfWeekField.parse(tokens, oneBasedDayOfWeek);
if (tokens.hasNext()) {
year = DefaultField.parse(tokens, 0, 0);
} else {
year = MatchAllField.instance;
}
if (!allowBothDayFields && !dayOfMonth.isUnspecified() && !dayOfWeek.isUnspecified()) {
throw new IllegalArgumentException("Day of month and day of week may not both be specified");
}
}
@Override
public boolean matches(ZonedDateTime t) {
return second.contains(t.getSecond()) &&
minute.contains(t.getMinute()) &&
hour.contains(t.getHour()) &&
month.contains(t.getMonthValue()) &&
year.contains(t.getYear()) &&
dayOfWeek.matches(t) &&
dayOfMonth.matches(t);
}
@Override
public ZonedDateTime nextExecution(ZonedDateTime from,
ZonedDateTime to) {
ZonedDateTime next = second instanceof MatchAllField ?
from.plusMinutes(1).truncatedTo(ChronoUnit.MINUTES) : from.plusSeconds(1).truncatedTo(ChronoUnit.SECONDS);
while (true) {
if (next.isBefore(from) || next.isAfter(to)) {
throw new IllegalStateException("out of range: " + from + " < " + next + " < " + to + " -> " + this);
}
SortedSet<Integer> set;
if (!year.contains(next.getYear())) {
if (!year.isFullRange()) {
set = year.getNumbers().tailSet(next.getYear());
if (set.isEmpty()) {
next = next.plusYears(1);
continue;
} else {
next = next.plusYears(set.first() - next.getYear());
}
}
}
if (!month.contains(next.getMonthValue())) {
if (!month.isFullRange()) {
set = month.getNumbers().tailSet(next.getMonthValue());
if (set.isEmpty()) {
next = next.plusMonths(1);
continue;
} else {
next = next.plusMonths(set.first() - next.getMonthValue());
}
}
}
if (!dayOfMonth.isUnspecified()) {
if (!dayOfMonth.contains(next.getDayOfMonth())) {
if (!dayOfMonth.isFullRange()) {
set = dayOfMonth.getNumbers().tailSet(next.getDayOfMonth());
if (set.isEmpty()) {
next = next.plusDays(1).truncatedTo(ChronoUnit.DAYS);
continue;
} else {
next = next.plusDays(set.first() - next.getDayOfMonth())
.truncatedTo(ChronoUnit.DAYS);
}
}
}
}
if (!dayOfWeek.isUnspecified()) {
if (!dayOfWeek.contains(next.getDayOfWeek().getValue())) {
if (!dayOfWeek.isFullRange()) {
set = dayOfWeek.getNumbers().tailSet(next.getDayOfWeek().getValue());
if (set.isEmpty()) {
next = next.plusDays(1).truncatedTo(ChronoUnit.DAYS);
continue;
} else {
DayOfWeek dayOfWeek = DayOfWeek.of(set.first());
next = next.with(TemporalAdjusters.next(dayOfWeek));
}
}
}
}
if (!hour.contains(next.getHour())) {
if (!hour.isFullRange()) {
set = hour.getNumbers().tailSet(next.getHour());
if (set.isEmpty()) {
next = next.plusHours(1).truncatedTo(ChronoUnit.HOURS);
continue;
} else {
next = next.plusHours(set.first() - next.getHour())
.truncatedTo(ChronoUnit.HOURS);
}
}
}
if (!minute.contains(next.getMinute())) {
if (!minute.isFullRange()) {
set = minute.getNumbers().tailSet(next.getMinute());
if (set.isEmpty()) {
next = next.plusMinutes(1).truncatedTo(ChronoUnit.MINUTES);
continue;
} else {
next = next.plusMinutes(set.first() - next.getMinute())
.truncatedTo(ChronoUnit.MINUTES);
}
}
}
if (!(second instanceof MatchAllField)) {
if (!second.contains(next.getSecond())) {
if (!second.isFullRange()) {
set = second.getNumbers().tailSet(next.getSecond());
if (set.isEmpty()) {
next = next.plusSeconds(1).truncatedTo(ChronoUnit.SECONDS);
continue;
} else {
next = next.plusSeconds(set.first() - next.getSecond())
.truncatedTo(ChronoUnit.SECONDS);
}
}
}
}
break;
}
return next;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
DefaultCronExpression that = (DefaultCronExpression) o;
if (!Objects.equals(dayOfMonth, that.dayOfMonth)) {
return false;
}
if (!Objects.equals(dayOfWeek, that.dayOfWeek)) {
return false;
}
if (!Objects.equals(hour, that.hour)) {
return false;
}
if (!Objects.equals(minute, that.minute)) {
return false;
}
if (!Objects.equals(month, that.month)) {
return false;
}
if (!Objects.equals(second, that.second)) {
return false;
}
if (!string.equals(that.string)) {
return false;
}
return Objects.equals(year, that.year);
}
@Override
public int hashCode() {
int result = string.hashCode();
result = 31 * result + (second != null ? second.hashCode() : 0);
result = 31 * result + (minute != null ? minute.hashCode() : 0);
result = 31 * result + (hour != null ? hour.hashCode() : 0);
result = 31 * result + (month != null ? month.hashCode() : 0);
result = 31 * result + (year != null ? year.hashCode() : 0);
result = 31 * result + (dayOfWeek != null ? dayOfWeek.hashCode() : 0);
result = 31 * result + (dayOfMonth != null ? dayOfMonth.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "CronExpression[" + string + " -> seconds=" + second +
",mins=" + minute +
",hrs=" + hour +
",dayOfMonths=" + dayOfMonth +
",dayOfWeek=" + dayOfWeek +
",months=" + month +
",yrs=" + year +
"]";
}
}

View file

@ -0,0 +1,177 @@
package org.xbib.time.schedule;
import java.util.NavigableSet;
import java.util.Objects;
import java.util.TreeSet;
public class DefaultField implements TimeField {
private final boolean fullRange;
private final NavigableSet<Integer> numbers;
DefaultField(Builder b) {
fullRange = b.fullRange;
numbers = fullRange ? null : b.numbers;
}
public static DefaultField parse(Tokens s, int min, int max) {
return new Builder(min, max).parse(s).build();
}
@Override
public boolean contains(int number) {
return fullRange || numbers.contains(number);
}
@Override
public NavigableSet<Integer> getNumbers() {
return numbers;
}
@Override
public boolean isFullRange() {
return fullRange;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
DefaultField that = (DefaultField) o;
if (fullRange != that.fullRange) {
return false;
}
return Objects.equals(numbers, that.numbers);
}
@Override
public int hashCode() {
int result = (fullRange ? 1 : 0);
result = 31 * result + (numbers != null ? numbers.hashCode() : 0);
return result;
}
@Override
public String toString() {
return isFullRange() ? "*" : getNumbers().toString();
}
public static class Builder {
private final NavigableSet<Integer> numbers;
private final int min;
private final int max;
private boolean fullRange;
Builder(int min, int max) {
this.min = min;
this.max = max;
numbers = new TreeSet<>();
}
protected Builder parse(Tokens tokens) {
Token token;
while (!endOfField(token = tokens.next())) {
if (parseValue(tokens, token, min, max)) {
break;
}
}
return this;
}
protected boolean parseValue(Tokens tokens, Token token, int first, int last) {
if (token == Token.NUMBER) {
return parseNumber(tokens, tokens.next(), tokens.number(), last);
} else if (token == Token.MATCH_ALL) {
token = tokens.next();
if (token == Token.SKIP) {
rangeSkip(first, last, nextNumber(tokens));
} else if (token == Token.VALUE_SEPARATOR) {
range(first, last);
} else if (endOfField(token)) {
range(first, last);
return true;
}
}
return false;
}
/**
* Returns true if the end of this field has been reached.
* @param tokens tokens
* @param token token
* @param first first
* @param last last
* @return true if end reached
*/
protected boolean parseNumber(Tokens tokens, Token token, int first, int last) {
Token t = token;
int l = last;
if (t == Token.SKIP) {
rangeSkip(first, l, nextNumber(tokens));
} else if (t == Token.RANGE) {
l = nextNumber(tokens);
t = tokens.next();
if (t == Token.SKIP) {
rangeSkip(first, l, nextNumber(tokens));
} else if (t == Token.VALUE_SEPARATOR) {
range(first, l);
} else if (endOfField(t)) {
range(first, l);
return true;
}
} else if (t == Token.VALUE_SEPARATOR) {
add(first);
} else if (endOfField(t)) {
add(first);
return true;
}
return false;
}
int nextNumber(Tokens tokens) {
if (tokens.next() == Token.NUMBER) {
return tokens.number();
}
throw new IllegalStateException("Expected number");
}
private boolean endOfField(Token token) {
return token == Token.FIELD_SEPARATOR || token == Token.END_OF_INPUT;
}
void rangeSkip(int first, int last, int skip) {
for (int i = first; i <= last; i++) {
if ((i - min) % skip == 0) {
add(i);
}
}
}
protected void range(int first, int last) {
if (first == min && last == max) {
fullRange = true;
} else {
for (int i = first; i <= last; i++) {
add(i);
}
}
}
protected void add(int value) {
numbers.add(value);
}
public DefaultField build() {
return new DefaultField(this);
}
}
}

View file

@ -0,0 +1,57 @@
package org.xbib.time.schedule;
import java.time.ZonedDateTime;
import java.util.concurrent.Callable;
class Entry<T> {
private String name;
private CronExpression cronExpression;
private Callable<T> callable;
private ZonedDateTime lastCalled;
private ZonedDateTime nextCall;
Entry(String name, CronExpression cronExpression, Callable<T> callable) {
this.name = name;
this.cronExpression = cronExpression;
this.callable = callable;
}
public String getName() {
return name;
}
public CronExpression getCronExpression() {
return cronExpression;
}
public Callable<T> getCallable() {
return callable;
}
public void setLastCalled(ZonedDateTime lastCalled) {
this.lastCalled = lastCalled;
// heuristic, limit to 1 year ahead
this.nextCall = cronExpression.nextExecution(lastCalled, lastCalled.plusYears(1));
}
public ZonedDateTime getLastCalled() {
return lastCalled;
}
public ZonedDateTime getNextCall() {
return nextCall;
}
@Override
public String toString() {
return "Entry[name=" + name + ", expression=" + cronExpression +
",callable= " + callable +
",lastcalled=" + lastCalled +
",nextcall=" + nextCall + "]";
}
}

View file

@ -0,0 +1,42 @@
package org.xbib.time.schedule;
import java.util.Arrays;
final class Keywords {
private final int[][][] keywords = new int[26][26][26];
public Keywords() {
for (int[][] second : keywords) {
for (int[] third : second) {
Arrays.fill(third, -1);
}
}
}
public void put(String keyword, int value) {
keywords[letterAt(keyword, 0)][letterAt(keyword, 1)][letterAt(keyword, 2)] = value;
}
public int get(String s, int start, int end) {
if (end - start != 3) {
throw new IllegalArgumentException();
}
int number = keywords[arrayIndex(s, start)][arrayIndex(s, start + 1)][arrayIndex(s, start + 2)];
if (number >= 0) {
return number;
}
throw new IllegalArgumentException();
}
private int arrayIndex(String s, int charIndex) {
int index = letterAt(s, charIndex);
if (index < 0 || index >= keywords.length) {
throw new IllegalArgumentException();
}
return index;
}
private static int letterAt(String s, int charIndex) {
return s.charAt(charIndex) - 'A';
}
}

View file

@ -0,0 +1,23 @@
package org.xbib.time.schedule;
import java.util.NavigableSet;
public enum MatchAllField implements TimeField {
instance;
@Override
public boolean contains(int number) {
return true;
}
@Override
public NavigableSet<Integer> getNumbers() {
return null;
}
@Override
public boolean isFullRange() {
return true;
}
}

View file

@ -0,0 +1,48 @@
package org.xbib.time.schedule;
public class MonthField extends DefaultField {
private MonthField(Builder b) {
super(b);
}
public static MonthField parse(Tokens s) {
return new Builder().parse(s).build();
}
public static class Builder extends DefaultField.Builder {
protected static final Keywords KEYWORDS = new Keywords();
static {
KEYWORDS.put("JAN", 1);
KEYWORDS.put("FEB", 2);
KEYWORDS.put("MAR", 3);
KEYWORDS.put("APR", 4);
KEYWORDS.put("MAY", 5);
KEYWORDS.put("JUN", 6);
KEYWORDS.put("JUL", 7);
KEYWORDS.put("AUG", 8);
KEYWORDS.put("SEP", 9);
KEYWORDS.put("OCT", 10);
KEYWORDS.put("NOV", 11);
KEYWORDS.put("DEC", 12);
}
Builder() {
super(1, 12);
}
@Override
protected Builder parse(Tokens tokens) {
tokens.keywords(KEYWORDS);
super.parse(tokens);
tokens.reset();
return this;
}
@Override
public MonthField build() {
return new MonthField(this);
}
}
}

View file

@ -0,0 +1,26 @@
package org.xbib.time.schedule;
import java.time.ZonedDateTime;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Matches once only.
*/
final class RebootCronExpression extends CronExpression {
private final AtomicBoolean matchOnce;
RebootCronExpression() {
matchOnce = new AtomicBoolean(true);
}
@Override
public boolean matches(ZonedDateTime t) {
return matchOnce.getAndSet(false);
}
@Override
public ZonedDateTime nextExecution(ZonedDateTime from, ZonedDateTime to) {
return null;
}
}

View file

@ -0,0 +1,12 @@
package org.xbib.time.schedule;
import java.util.NavigableSet;
public interface TimeField {
boolean contains(int number);
NavigableSet<Integer> getNumbers();
boolean isFullRange();
}

View file

@ -0,0 +1,15 @@
package org.xbib.time.schedule;
enum Token {
END_OF_INPUT,
FIELD_SEPARATOR,
LAST,
MATCH_ALL,
MATCH_ONE,
NTH,
NUMBER,
RANGE,
SKIP,
VALUE_SEPARATOR,
WEEKDAY
}

View file

@ -0,0 +1,189 @@
package org.xbib.time.schedule;
final class Tokens {
private int number;
private int offset;
private Keywords keywords;
private final String source;
private final int length;
private int position;
public Tokens(String s) {
source = s;
length = s.length();
position = 0;
}
public int number() {
return number;
}
public void offset(int offset) {
this.offset = offset;
}
public void keywords(Keywords k) {
keywords = k;
}
public void reset() {
offset = 0;
keywords = null;
}
public boolean hasNext() {
return hasNextChar();
}
public Token next() {
if (position >= length) {
return Token.END_OF_INPUT;
}
int start = position;
char c = currentChar();
switch (c) {
case ' ':
case '\t':
do {
if (!hasNextChar()) {
position++;
break;
}
c = nextChar();
} while (isWhitespace(c));
return Token.FIELD_SEPARATOR;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
do {
if (!hasNextChar()) {
position++;
break;
}
c = nextChar();
} while (isDigit(c));
number = Integer.parseInt(substringFrom(start)) - offset;
return Token.NUMBER;
case ',':
position++;
return Token.VALUE_SEPARATOR;
case '*':
position++;
return Token.MATCH_ALL;
case '-':
position++;
return Token.RANGE;
case '/':
position++;
return Token.SKIP;
case 'A':
case 'B':
case 'C':
case 'D':
case 'E':
case 'F':
case 'G':
case 'H':
case 'I':
case 'J':
case 'K':
case 'L':
case 'M':
case 'N':
case 'O':
case 'P':
case 'Q':
case 'R':
case 'S':
case 'T':
case 'U':
case 'V':
case 'W':
case 'X':
case 'Y':
case 'Z':
do {
if (!hasNextChar()) {
position++;
break;
}
c = nextChar();
} while (isLetter(c));
if (position - start == 1) {
c = source.charAt(start);
if (c == 'L') {
return Token.LAST;
} else if (c == 'W') {
return Token.WEEKDAY;
}
throw new IllegalArgumentException(badCharacter(c, start));
} else {
if (keywords != null) {
try {
int mapped = keywords.get(source, start, position);
if (mapped != -1) {
number = mapped;
return Token.NUMBER;
}
} catch (IllegalArgumentException ignore) {
}
}
throw new IllegalArgumentException(badKeyword(start));
}
case '?':
position++;
return Token.MATCH_ONE;
case '#':
position++;
return Token.NTH;
}
throw new IllegalArgumentException(badCharacter(c, position));
}
private String badCharacter(char c, int index) {
return "Bad character '" + c + "' at position " + index + " in string: " + source;
}
private String badKeyword(int start) {
return "Bad keyword '" + substringFrom(start) + "' at position " + start + " in string: " + source;
}
private String substringFrom(int start) {
return source.substring(start, position);
}
private boolean hasNextChar() {
return position < length - 1;
}
private char nextChar() {
return source.charAt(++position);
}
private char currentChar() {
return source.charAt(position);
}
private static boolean isLetter(char c) {
return 'A' <= c && c <= 'Z';
}
private static boolean isDigit(char c) {
return '0' <= c && c <= '9';
}
private static boolean isWhitespace(char c) {
return c == ' ' || c == '\t';
}
}

View file

@ -0,0 +1,4 @@
/**
* Schedule jobs (like cron).
*/
package org.xbib.time.schedule;

View file

@ -0,0 +1,126 @@
package org.xbib.time.util;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
/**
* Abstract multi map.
*
* @param <K> the key type parameter
* @param <V> the value type parameter
*/
abstract class AbstractMultiMap<K, V> implements MultiMap<K, V> {
private final Map<K, Collection<V>> map;
AbstractMultiMap() {
this(null);
}
private AbstractMultiMap(MultiMap<K, V> map) {
this.map = newMap();
if (map != null) {
putAll(map);
}
}
@Override
public int size() {
return map.size();
}
@Override
public void clear() {
map.clear();
}
@Override
public boolean isEmpty() {
return map.isEmpty();
}
@Override
public boolean containsKey(K key) {
return map.containsKey(key);
}
@Override
public Set<K> keySet() {
return map.keySet();
}
@Override
public boolean put(K key, V value) {
Collection<V> set = map.get(key);
if (set == null) {
set = newValues();
set.add(value);
map.put(key, set);
return true;
} else {
set.add(value);
return false;
}
}
@Override
public void putAll(K key, Iterable<V> values) {
if (values == null) {
return;
}
Collection<V> set = map.computeIfAbsent(key, k -> newValues());
for (V v : values) {
set.add(v);
}
}
@Override
public Collection<V> get(K key) {
return map.get(key);
}
@Override
public Collection<V> remove(K key) {
return map.remove(key);
}
@Override
public boolean remove(K key, V value) {
Collection<V> set = map.get(key);
return set != null && set.remove(value);
}
@Override
public void putAll(MultiMap<K, V> map) {
if (map != null) {
for (K key : map.keySet()) {
putAll(key, map.get(key));
}
}
}
@Override
public Map<K, Collection<V>> asMap() {
return map;
}
@Override
public boolean equals(Object obj) {
return obj instanceof AbstractMultiMap && map.equals(((AbstractMultiMap) obj).map);
}
@Override
public int hashCode() {
return map.hashCode();
}
@Override
public String toString() {
return map.toString();
}
abstract Collection<V> newValues();
abstract Map<K, Collection<V>> newMap();
}

View file

@ -0,0 +1,29 @@
package org.xbib.time.util;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
/**
* Linked multi map.
*
* @param <K> the key type parameter
* @param <V> the value type parameter
*/
public class LinkedHashSetMultiMap<K, V> extends AbstractMultiMap<K, V> {
public LinkedHashSetMultiMap() {
super();
}
@Override
Collection<V> newValues() {
return new LinkedHashSet<>();
}
@Override
Map<K, Collection<V>> newMap() {
return new LinkedHashMap<>();
}
}

View file

@ -0,0 +1,38 @@
package org.xbib.time.util;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
/**
* MultiMap interface.
*
* @param <K> the key type parameter
* @param <V> the value type parameter
*/
public interface MultiMap<K, V> {
void clear();
int size();
boolean isEmpty();
boolean containsKey(K key);
Collection<V> get(K key);
Set<K> keySet();
boolean put(K key, V value);
void putAll(K key, Iterable<V> values);
void putAll(MultiMap<K, V> map);
Collection<V> remove(K key);
boolean remove(K key, V value);
Map<K, Collection<V>> asMap();
}

View file

@ -0,0 +1,31 @@
package org.xbib.time.util;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.TreeMap;
/**
* A {@link TreeMap} based multi map. The keys ore ordered by a comparator.
* @param <K> te key type
* @param <V> the value type
*/
public class TreeMultiMap<K, V> extends AbstractMultiMap<K, V> {
private final Comparator<K> comparator;
public TreeMultiMap(Comparator<K> comparator) {
this.comparator = comparator;
}
@Override
Map<K, Collection<V>> newMap() {
return new TreeMap<>(comparator);
}
@Override
Collection<V> newValues() {
return new LinkedHashSet<>();
}
}

View file

@ -0,0 +1,4 @@
/**
*
*/
package org.xbib.time.util;

View file

@ -10,7 +10,7 @@ import java.util.Locale;
import static org.junit.Assert.assertEquals;
public class FormatterTest {
public class DateTimeFormatterTest {
@Test
public void testLocalDate() {

View file

@ -10,8 +10,10 @@ import static org.junit.Assert.assertNotNull;
public class PrettyTimeAPIManipulationTest {
List<TimeUnitQuantity> list = null;
PrettyTime t = new PrettyTime();
private List<TimeUnitQuantity> list = null;
private PrettyTime t = new PrettyTime();
private TimeUnitQuantity timeUnitQuantity = null;
@Test

View file

@ -16,10 +16,13 @@ import static org.junit.Assert.assertTrue;
public class PrettyTimeI18n_AR_Test {
private Locale defaultLocale;
private Locale locale;
@Before
public void setUp() throws Exception {
defaultLocale = Locale.getDefault();
locale = new Locale("ar");
Locale.setDefault(locale);
}
@ -244,7 +247,6 @@ public class PrettyTimeI18n_AR_Test {
@After
public void tearDown() throws Exception {
Locale.setDefault(locale);
Locale.setDefault(defaultLocale);
}
}

View file

@ -14,14 +14,15 @@ import static org.junit.Assert.assertEquals;
public class PrettyTimeI18n_BG_Test {
// Stores current locale so that it can be restored
private Locale defaultLocale;
private Locale locale;
// Method setUp() is called automatically before every test method
@Before
public void setUp() throws Exception {
locale = Locale.getDefault();
Locale.setDefault(new Locale("bg"));
defaultLocale = Locale.getDefault();
locale = new Locale("bg");
Locale.setDefault(locale);
}
@Test
@ -215,10 +216,9 @@ public class PrettyTimeI18n_BG_Test {
assertEquals("след 3 дни 15 часа 38 минути", t.format(timeUnitQuantities));
}
// Method tearDown() is called automatically after every test method
@After
public void tearDown() throws Exception {
Locale.setDefault(locale);
Locale.setDefault(defaultLocale);
}
}

View file

@ -1,7 +1,7 @@
package org.xbib.time.pretty;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.time.Instant;
@ -15,16 +15,14 @@ import static org.junit.Assert.assertTrue;
public class PrettyTimeI18n_CA_Test {
protected static Locale locale;
private Locale defaultLocale;
@BeforeClass
public static void setUp() throws Exception {
locale = Locale.getDefault();
Locale.setDefault(new Locale("ca"));
}
private Locale locale;
@AfterClass
public static void tearDown() throws Exception {
@Before
public void setUp() throws Exception {
defaultLocale = Locale.getDefault();
locale = new Locale("ca");
Locale.setDefault(locale);
}
@ -230,4 +228,8 @@ public class PrettyTimeI18n_CA_Test {
assertEquals("vor 3 Jahrzehnten", t.format((0)));
}
@After
public void tearDown() throws Exception {
Locale.setDefault(defaultLocale);
}
}

View file

@ -1,8 +1,8 @@
package org.xbib.time.pretty;
import org.junit.AfterClass;
import org.junit.After;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Before;
import org.junit.Test;
import org.xbib.time.pretty.units.JustNow;
import org.xbib.time.pretty.units.Month;
@ -17,16 +17,15 @@ import java.util.Locale;
import static org.junit.Assert.assertEquals;
public class PrettyTimeI18n_CS_Test {
private static Locale locale;
@BeforeClass
public static void setUp() throws Exception {
locale = Locale.getDefault();
Locale.setDefault(new Locale("cs"));
}
private Locale defaultLocale;
@AfterClass
public static void tearDown() throws Exception {
private Locale locale;
@Before
public void setUp() throws Exception {
defaultLocale = Locale.getDefault();
locale = new Locale("cs");
Locale.setDefault(locale);
}
@ -213,7 +212,7 @@ public class PrettyTimeI18n_CS_Test {
/**
* Tests formatApproximateDuration and by proxy, formatDuration.
*
* @throws Exception
* @throws Exception exception
*/
@Test
public void testFormatApproximateDuration() throws Exception {
@ -224,4 +223,8 @@ public class PrettyTimeI18n_CS_Test {
Assert.assertEquals("10 minutami", result);
}
@After
public void tearDown() throws Exception {
Locale.setDefault(defaultLocale);
}
}

View file

@ -1,5 +1,6 @@
package org.xbib.time.pretty;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@ -9,11 +10,16 @@ import java.util.Locale;
import static org.junit.Assert.assertEquals;
public class PrettyTimeI18n_DA_Test {
private Locale defaultLocale;
private Locale locale;
@Before
public void setUp() throws Exception {
defaultLocale = Locale.getDefault();
locale = new Locale("da");
Locale.setDefault(locale);
}
@Test
@ -152,4 +158,9 @@ public class PrettyTimeI18n_DA_Test {
PrettyTime t = new PrettyTime((3155692597470L * 3L), locale);
assertEquals("3 århundreder siden", t.format(0));
}
@After
public void tearDown() throws Exception {
Locale.setDefault(defaultLocale);
}
}

View file

@ -1,5 +1,6 @@
package org.xbib.time.pretty;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.xbib.time.pretty.units.JustNow;
@ -14,11 +15,16 @@ import java.util.Locale;
import static org.junit.Assert.assertEquals;
public class PrettyTimeI18n_ET_Test {
private Locale defaultLocale;
private Locale locale;
@Before
public void setUp() throws Exception {
defaultLocale = Locale.getDefault();
locale = new Locale("et");
Locale.setDefault(locale);
}
@Test
@ -333,4 +339,8 @@ public class PrettyTimeI18n_ET_Test {
return t;
}
@After
public void tearDown() throws Exception {
Locale.setDefault(defaultLocale);
}
}

View file

@ -15,12 +15,13 @@ import static org.junit.Assert.assertTrue;
public class PrettyTimeI18n_FA_Test {
// Stores current locale so that it can be restored
private Locale defaultLocale;
private Locale locale;
// Method setUp() is called automatically before every test method
@Before
public void setUp() throws Exception {
defaultLocale = Locale.getDefault();
locale = new Locale("fa");
Locale.setDefault(locale);
}
@ -230,10 +231,8 @@ public class PrettyTimeI18n_FA_Test {
assertEquals("3 دهه پیش", t.format((0)));
}
// Method tearDown() is called automatically after every test method
@After
public void tearDown() throws Exception {
Locale.setDefault(locale);
Locale.setDefault(defaultLocale);
}
}

View file

@ -1,5 +1,6 @@
package org.xbib.time.pretty;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.xbib.time.pretty.units.JustNow;
@ -14,11 +15,16 @@ import java.util.Locale;
import static org.junit.Assert.assertEquals;
public class PrettyTimeI18n_FI_Test {
private Locale defaultLocale;
private Locale locale;
@Before
public void setUp() throws Exception {
defaultLocale = Locale.getDefault();
locale = new Locale("fi");
Locale.setDefault(locale);
}
@Test
@ -333,4 +339,9 @@ public class PrettyTimeI18n_FI_Test {
}
return t;
}
@After
public void tearDown() throws Exception {
Locale.setDefault(defaultLocale);
}
}

View file

@ -12,37 +12,34 @@ import java.util.Locale;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/**
* All the tests for PrettyTime.
*/
public class PrettyTimeI18n_FR_Test {
// Stores current locale so that it can be restored
private Locale defaultLocale;
private Locale locale;
// Method setUp() is called automatically before every test method
@Before
public void setUp() throws Exception {
locale = Locale.getDefault();
defaultLocale = Locale.getDefault();
locale = Locale.FRENCH;
Locale.setDefault(locale);
}
@Test
public void testPrettyTimeFRENCH() {
// The FRENCH resource bundle should be used
PrettyTime p = new PrettyTime(Locale.FRENCH);
PrettyTime p = new PrettyTime(locale);
assertEquals("à l'instant", p.format(LocalDateTime.now()));
}
@Test
public void testPrettyTimeFRENCHCenturies() {
PrettyTime p = new PrettyTime((3155692597470L * 3L), Locale.FRENCH);
PrettyTime p = new PrettyTime((3155692597470L * 3L), locale);
assertEquals(p.format(0), "il y a 3 siècles");
}
@Test
public void testPrettyTimeViaDefaultLocaleFRENCH() {
// The FRENCH resource bundle should be used
Locale.setDefault(Locale.FRENCH);
PrettyTime p = new PrettyTime();
assertEquals(p.format(LocalDateTime.now()), "à l'instant");
}
@ -50,7 +47,7 @@ public class PrettyTimeI18n_FR_Test {
@Test
public void testPrettyTimeFRENCHLocale() {
long t = 1L;
PrettyTime p = new PrettyTime((0), Locale.FRENCH);
PrettyTime p = new PrettyTime((0), locale);
while (1000L * 60L * 60L * 24L * 365L * 1000000L > t) {
LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault());
assertTrue(p.format(localDateTime).startsWith("dans") || p.format(localDateTime).startsWith("à l'instant"));
@ -58,10 +55,9 @@ public class PrettyTimeI18n_FR_Test {
}
}
// Method tearDown() is called automatically after every test method
@After
public void tearDown() throws Exception {
Locale.setDefault(locale);
Locale.setDefault(defaultLocale);
}
}

View file

@ -1,5 +1,6 @@
package org.xbib.time.pretty;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.xbib.time.pretty.units.JustNow;
@ -14,11 +15,16 @@ import java.util.Locale;
import static org.junit.Assert.assertEquals;
public class PrettyTimeI18n_IT_Test {
private Locale defaultLocale;
private Locale locale;
@Before
public void setUp() throws Exception {
defaultLocale = Locale.getDefault();
locale = new Locale("it");
Locale.setDefault(locale);
}
@Test
@ -333,4 +339,9 @@ public class PrettyTimeI18n_IT_Test {
}
return t;
}
@After
public void tearDown() throws Exception {
Locale.setDefault(defaultLocale);
}
}

View file

@ -15,13 +15,15 @@ import static org.junit.Assert.assertTrue;
public class PrettyTimeI18n_KO_Test {
private Locale defaultLocale;
private Locale locale;
// Method setUp() is called automatically before every test method
@Before
public void setUp() throws Exception {
locale = Locale.getDefault();
Locale.setDefault(Locale.KOREA);
defaultLocale = Locale.getDefault();
locale = Locale.KOREA;
Locale.setDefault(locale);
}
@Test
@ -227,10 +229,8 @@ public class PrettyTimeI18n_KO_Test {
assertEquals("vor 3 Jahrzehnten", t.format((0)));
}
// Method tearDown() is called automatically after every test method
@After
public void tearDown() throws Exception {
Locale.setDefault(locale);
Locale.setDefault(defaultLocale);
}
}

View file

@ -1,5 +1,6 @@
package org.xbib.time.pretty;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@ -11,11 +12,16 @@ import java.util.Locale;
import static org.junit.Assert.assertEquals;
public class PrettyTimeI18n_NL_Test {
private Locale defaultLocale;
private Locale locale;
@Before
public void setUp() throws Exception {
defaultLocale = Locale.getDefault();
locale = new Locale("nl");
Locale.setDefault(locale);
}
@Test
@ -159,4 +165,9 @@ public class PrettyTimeI18n_NL_Test {
PrettyTime t = new PrettyTime((3155692597470L * 3L), locale);
assertEquals("3 eeuwen geleden", t.format((0)));
}
@After
public void tearDown() throws Exception {
Locale.setDefault(defaultLocale);
}
}

View file

@ -1,5 +1,6 @@
package org.xbib.time.pretty;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@ -12,11 +13,15 @@ import static org.junit.Assert.assertEquals;
public class PrettyTimeI18n_NO_Test {
private Locale defaultLocale;
private Locale locale;
@Before
public void setUp() throws Exception {
defaultLocale = Locale.getDefault();
locale = new Locale("no");
Locale.setDefault(locale);
}
@Test
@ -160,4 +165,9 @@ public class PrettyTimeI18n_NO_Test {
PrettyTime t = new PrettyTime((3155692597470L * 3L), locale);
assertEquals("3 århundre siden", t.format((0)));
}
@After
public void tearDown() throws Exception {
Locale.setDefault(defaultLocale);
}
}

View file

@ -13,10 +13,13 @@ import static org.junit.Assert.assertEquals;
public class PrettyTimeI18n_RU_Test {
private Locale defaultLocale;
private Locale locale;
@Before
public void setUp() throws Exception {
defaultLocale = Locale.getDefault();
locale = new Locale("ru");
Locale.setDefault(locale);
}
@ -163,9 +166,8 @@ public class PrettyTimeI18n_RU_Test {
assertEquals("3 века назад", t.format((0)));
}
@After
public void tearDown() throws Exception {
Locale.setDefault(locale);
Locale.setDefault(defaultLocale);
}
}

View file

@ -1,5 +1,6 @@
package org.xbib.time.pretty;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@ -12,11 +13,15 @@ import static org.junit.Assert.assertEquals;
public class PrettyTimeI18n_SV_Test {
private Locale defaultLocale;
private Locale locale;
@Before
public void setUp() throws Exception {
defaultLocale = Locale.getDefault();
locale = new Locale("sv");
Locale.setDefault(locale);
}
@Test
@ -161,4 +166,9 @@ public class PrettyTimeI18n_SV_Test {
PrettyTime t = new PrettyTime((3155692597470L * 3L), locale);
assertEquals("3 århundraden sedan", t.format((0)));
}
@After
public void tearDown() throws Exception {
Locale.setDefault(defaultLocale);
}
}

View file

@ -10,13 +10,12 @@ import java.time.ZoneId;
import java.util.Locale;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class PrettyTimeI18n_Test {
// Stores current locale so that it can be restored
private Locale locale;
// Method setUp() is called automatically before every test method
@Before
public void setUp() throws Exception {
locale = Locale.getDefault();
@ -105,7 +104,7 @@ public class PrettyTimeI18n_Test {
PrettyTime p = new PrettyTime(0, Locale.ROOT);
LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault());
while (1000L * 60L * 60L * 24L * 365L * 1000000L > t) {
assertEquals(p.format(localDateTime).endsWith("now"), true);
assertTrue(p.format(localDateTime).endsWith("now"));
t *= 2L;
}
}
@ -116,12 +115,11 @@ public class PrettyTimeI18n_Test {
PrettyTime p = new PrettyTime(0, Locale.GERMAN);
LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(0), ZoneId.systemDefault());
while (1000L * 60L * 60L * 24L * 365L * 1000000L > t) {
assertEquals(p.format(localDateTime).startsWith("in") || p.format(localDateTime).startsWith("Jetzt"), true);
assertTrue(p.format(localDateTime).startsWith("in") || p.format(localDateTime).startsWith("Jetzt"));
t *= 2L;
}
}
// Method tearDown() is called automatically after every test method
@After
public void tearDown() throws Exception {
Locale.setDefault(locale);

View file

@ -13,10 +13,13 @@ import static org.junit.Assert.assertEquals;
public class PrettyTimeI18n_UA_Test {
private Locale defaultLocale;
private Locale locale;
@Before
public void setUp() throws Exception {
defaultLocale = Locale.getDefault();
locale = new Locale("ua");
Locale.setDefault(locale);
}
@ -189,6 +192,6 @@ public class PrettyTimeI18n_UA_Test {
@After
public void tearDown() throws Exception {
Locale.setDefault(locale);
Locale.setDefault(defaultLocale);
}
}

View file

@ -15,10 +15,13 @@ import static org.junit.Assert.assertTrue;
public class PrettyTimeI18n_hi_IN_Test {
private Locale defaultLocale;
private Locale locale;
@Before
public void setUp() throws Exception {
defaultLocale = Locale.getDefault();
locale = new Locale("hi", "IN");
Locale.setDefault(locale);
}
@ -27,7 +30,7 @@ public class PrettyTimeI18n_hi_IN_Test {
public void testLocaleISOCorrectness() {
assertEquals("hi", this.locale.getLanguage());
assertEquals("IN", this.locale.getCountry());
assertEquals("हिदी", this.locale.getDisplayLanguage());
assertEquals("हिन्दी", this.locale.getDisplayLanguage());
assertEquals("भारत", this.locale.getDisplayCountry());
}
@ -243,6 +246,6 @@ public class PrettyTimeI18n_hi_IN_Test {
@After
public void tearDown() throws Exception {
Locale.setDefault(Locale.ENGLISH);
Locale.setDefault(defaultLocale);
}
}

View file

@ -15,10 +15,13 @@ import static org.junit.Assert.assertTrue;
public class PrettyTimeI18n_in_ID_Test {
private Locale defaultLocale;
private Locale locale;
@Before
public void setUp() throws Exception {
defaultLocale = Locale.getDefault();
locale = new Locale("in", "ID");
Locale.setDefault(locale);
}
@ -242,6 +245,6 @@ public class PrettyTimeI18n_in_ID_Test {
@After
public void tearDown() throws Exception {
Locale.setDefault(Locale.ENGLISH);
Locale.setDefault(defaultLocale);
}
}

View file

@ -11,12 +11,15 @@ import static org.junit.Assert.assertEquals;
public class PrettyTimeI18n_zh_TW_Test {
private Locale defaultLocale;
private Locale locale;
@Before
public void setUp() throws Exception {
defaultLocale = Locale.getDefault();
locale = Locale.TRADITIONAL_CHINESE;
Locale.setDefault(Locale.TRADITIONAL_CHINESE);
Locale.setDefault(locale);
}
@Test
@ -185,6 +188,6 @@ public class PrettyTimeI18n_zh_TW_Test {
@After
public void tearDown() throws Exception {
Locale.setDefault(locale);
Locale.setDefault(defaultLocale);
}
}

View file

@ -11,13 +11,11 @@ import static org.junit.Assert.assertEquals;
public class PrettyTimeLocaleFallbackTest {
// Stores current locale so that it can be restored
private Locale locale;
private Locale defaultLocale;
// Method setUp() is called automatically before every test method
@Before
public void setUp() throws Exception {
locale = Locale.getDefault();
defaultLocale = Locale.getDefault();
Locale.setDefault(new Locale("Foo", "Bar"));
}
@ -32,7 +30,7 @@ public class PrettyTimeLocaleFallbackTest {
@After
public void tearDown() throws Exception {
Locale.setDefault(locale);
Locale.setDefault(defaultLocale);
}
}

View file

@ -15,11 +15,11 @@ import static org.junit.Assert.assertTrue;
public class PrettyTimeTest {
private Locale locale;
private Locale defaultLocale;
@Before
public void setUp() throws Exception {
locale = Locale.getDefault();
defaultLocale = Locale.getDefault();
Locale.setDefault(Locale.ROOT);
}
@ -236,7 +236,6 @@ public class PrettyTimeTest {
@After
public void tearDown() throws Exception {
Locale.setDefault(locale);
Locale.setDefault(defaultLocale);
}
}

View file

@ -17,11 +17,11 @@ import static org.junit.Assert.assertEquals;
public class PrettyTimeUnitConfigurationTest {
private Locale locale;
private Locale defaultLocale;
@Before
public void setUp() throws Exception {
locale = Locale.getDefault();
defaultLocale = Locale.getDefault();
Locale.setDefault(Locale.ROOT);
}
@ -53,7 +53,6 @@ public class PrettyTimeUnitConfigurationTest {
@After
public void tearDown() throws Exception {
Locale.setDefault(locale);
Locale.setDefault(defaultLocale);
}
}

View file

@ -13,11 +13,11 @@ import static org.junit.Assert.assertEquals;
public class SimpleTimeFormatTest {
private Locale locale;
private Locale defaultLocale;
@Before
public void setUp() throws Exception {
locale = Locale.getDefault();
defaultLocale = Locale.getDefault();
Locale.setDefault(Locale.ROOT);
}
@ -46,7 +46,7 @@ public class SimpleTimeFormatTest {
@After
public void tearDown() throws Exception {
Locale.setDefault(locale);
Locale.setDefault(defaultLocale);
}
}

View file

@ -12,12 +12,16 @@ import java.time.ZoneId;
import java.util.Locale;
public class SimpleTimeFormatTimeQuantifiedNameTest {
private Locale defaultLocale;
private Locale locale;
@Before
public void setUp() throws Exception {
locale = Locale.getDefault();
Locale.setDefault(new Locale("yy"));
defaultLocale = Locale.getDefault();
locale = new Locale("yy");
Locale.setDefault(locale);
}
@Test
@ -76,10 +80,8 @@ public class SimpleTimeFormatTimeQuantifiedNameTest {
Assert.assertEquals("1 hour ago", p.format(0));
}
// Method tearDown() is called automatically after every test method
@After
public void tearDown() throws Exception {
Locale.setDefault(locale);
Locale.setDefault(defaultLocale);
}
}

View file

@ -9,21 +9,26 @@ import java.util.Locale;
import java.util.ResourceBundle;
public class TimeFormatProviderTest {
@Test
public void test() {
Locale defaultLocale = Locale.getDefault();
Locale locale = new Locale("xx");
Locale.setDefault(locale);
ResourceBundle bundle = ResourceBundle.getBundle(Resources.class.getName(), locale);
Assert.assertTrue(bundle instanceof TimeFormatProvider);
Locale.setDefault(defaultLocale);
}
@Test
public void testFormatFromDirectFormatOverride() throws Exception {
Locale defaultLocale = Locale.getDefault();
Locale locale = new Locale("xx");
Locale.setDefault(locale);
PrettyTime prettyTime = new PrettyTime(locale);
String result = prettyTime.format(System.currentTimeMillis() + 1000 * 60 * 6);
Assert.assertEquals("6 minutes from now", result);
Locale.setDefault(defaultLocale);
}
}

View file

@ -0,0 +1,261 @@
package org.xbib.time.schedule;
import static org.junit.Assert.assertTrue;
import static org.xbib.time.schedule.DateTimes.toDates;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.DayOfWeek;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class CompareBehaviorToQuartzTest {
static final CronExpression.Parser quartzLike = CronExpression.parser()
.withSecondsField(true)
.withOneBasedDayOfWeek(true)
.allowBothDayFields(false);
private static final String timeFormatString = "s m H d M E yyyy";
private static final DateTimeFormatter dateTimeFormat = DateTimeFormatter.ofPattern(timeFormatString);
private final DateFormat dateFormat = new SimpleDateFormat(timeFormatString);
protected String string;
private Times expected;
@Before
public void before() {
expected = new Times();
}
@Test
public void complex() throws Exception {
string = "0/5 14,18,3-39,52 * ? JAN,MAR,SEP MON-FRI 2002-2010";
expected.seconds.with(0);
expected.minutes.with(52).withRange(3, 39);
expected.months.with(1, 3, 9);
expected.daysOfWeek.withRange(1, 5);
expected.years.withRange(2002, 2010);
check();
}
@Test
public void at_noon_every_day() throws Exception {
string = "0 0 12 * * ?";
expected.seconds.with(0);
expected.minutes.with(0);
expected.hours.with(12);
check();
}
@Test
public void at_10_15am_every_day1() throws Exception {
string = "0 15 10 ? * *";
expected.seconds.with(0);
expected.minutes.with(15);
expected.hours.with(10);
check();
}
@Test
public void at_10_15am_every_day2() throws Exception {
string = "0 15 10 * * ?";
expected.seconds.with(0);
expected.minutes.with(15);
expected.hours.with(10);
check();
}
@Test
public void at_10_15am_every_day3() throws Exception {
string = "0 15 10 * * ? *";
expected.seconds.with(0);
expected.minutes.with(15);
expected.hours.with(10);
check();
}
@Test
public void at_10_15am_every_day_in_2005() throws Exception {
string = "0 15 10 * * ? 2005";
expected.seconds.with(0);
expected.minutes.with(15);
expected.hours.with(10);
expected.years.with(2005);
check();
}
@Test
public void every_minute_of_2pm() throws Exception {
string = "0 * 14 * * ?";
expected.seconds.with(0);
expected.hours.with(14);
check();
}
@Test
public void every_5_minutes_of_2pm() throws Exception {
string = "0 0/5 14 * * ?";
expected.seconds.with(0);
expected.minutes.withRange(0, 59, 5);
expected.hours.with(14);
check();
}
@Test
public void every_5_minutes_of_2pm_and_6pm() throws Exception {
string = "0 0/5 14,18 * * ?";
expected.seconds.with(0);
expected.minutes.withRange(0, 59, 5);
expected.hours.with(14, 18);
check();
}
@Test
public void first_5_minutes_of_2pm() throws Exception {
string = "0 0-5 14 * * ?";
expected.seconds.with(0);
expected.minutes.withRange(0, 5);
expected.hours.with(14);
check();
}
@Test
public void at_2_10pm_and_2_44pm_every_wednesday_in_march() throws Exception {
string = "0 10,44 14 ? 3 WED";
expected.seconds.with(0);
expected.minutes.with(10, 44);
expected.hours.with(14);
expected.months.with(3);
expected.daysOfWeek.with(3);
check();
}
@Test
public void at_10_15am_every_weekday() throws Exception {
string = "0 15 10 ? * MON-FRI";
expected.seconds.with(0);
expected.minutes.with(15);
expected.hours.with(10);
expected.daysOfWeek.withRange(1, 5);
check();
}
@Test
public void at_10_15am_on_the_15th_of_every_month() throws Exception {
string = "0 15 10 15 * ?";
expected.seconds.with(0);
expected.minutes.with(15);
expected.hours.with(10);
expected.daysOfMonth.with(15);
check();
}
@Test
public void at_10_15am_on_the_last_day_of_every_month() throws Exception {
string = "0 15 10 L * ?";
List<ZonedDateTime> times = new ArrayList<>();
ZonedDateTime t = ZonedDateTime.now().withDayOfYear(1).truncatedTo(ChronoUnit.DAYS).plusHours(10).plusMinutes(15);
int year = t.getYear();
while (t.getYear() == year) {
times.add(t.with(TemporalAdjusters.lastDayOfMonth()));
t = t.plusMonths(1);
}
check(times);
}
@Test
public void at_10_15am_on_the_last_friday_of_every_month() throws Exception {
string = "0 15 10 ? * 6L";
List<ZonedDateTime> times = new ArrayList<>();
ZonedDateTime t = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS).plusHours(10).plusMinutes(15);
int year = t.getYear();
while (t.getYear() == year) {
times.add(DateTimes.lastOfMonth(t, DayOfWeek.FRIDAY));
t = t.plusMonths(1);
}
check(times);
}
@Test
public void at_10_15am_on_the_last_friday_of_every_month_during_2002_through_2005() throws Exception {
string = "0 15 10 ? * 6L 2002-2005";
List<ZonedDateTime> times = new ArrayList<>();
for (int year = 2002; year <= 2005; year++) {
ZonedDateTime t = ZonedDateTime.now().withYear(year).truncatedTo(ChronoUnit.DAYS).plusHours(10).plusMinutes(15);
while (t.getYear() == year) {
times.add(DateTimes.lastOfMonth(t, DayOfWeek.FRIDAY));
t = t.plusMonths(1);
}
}
check(times);
}
@Test
@Ignore
// TODO let's see if we can make this more reliably faster than the respective quartz run
public void at_10_15am_on_the_third_friday_of_every_month() throws Exception {
string = "0 15 10 ? * 6#3";
List<ZonedDateTime> times = new ArrayList<>();
ZonedDateTime t = ZonedDateTime.now().withDayOfYear(1).truncatedTo(ChronoUnit.DAYS).plusHours(10).plusMinutes(15);
int year = t.getYear();
while (t.getYear() == year) {
times.add(DateTimes.nthOfMonth(t, DayOfWeek.FRIDAY, 3));
t = t.plusMonths(1);
}
check(times);
}
@Test
public void at_noon_every_5_days_every_month_starting_on_the_first_day_of_the_month() throws Exception {
string = "0 0 12 1/5 * ?";
expected.seconds.with(0);
expected.minutes.with(0);
expected.hours.with(12);
expected.daysOfMonth.withRange(1, 31, 5);
check();
}
@Test
public void november_11th_at_11_11am() throws Exception {
string = "0 11 11 11 11 ?";
expected.seconds.with(0);
expected.minutes.with(11);
expected.hours.with(11);
expected.daysOfMonth.with(11);
expected.months.with(11);
check();
}
private void check() throws ParseException {
check(expected.dateTimes());
}
protected void check(Iterable<ZonedDateTime> times) throws ParseException {
checkLocalImplementation(times);
checkQuartzImplementation(toDates(times));
}
private void checkQuartzImplementation(Iterable<Date> times) throws ParseException {
org.quartz.CronExpression quartz = new org.quartz.CronExpression(string);
for (Date time : times) {
assertTrue(dateFormat.format(time).toUpperCase() + " doesn't match expression: " + string, quartz.isSatisfiedBy(time));
}
}
private void checkLocalImplementation(Iterable<ZonedDateTime> times) {
CronExpression expr = quartzLike.parse(string);
for (ZonedDateTime time : times) {
assertTrue(time.format(dateTimeFormat).toUpperCase() + " doesn't match expression: " + string, expr.matches(time));
}
}
}

View file

@ -0,0 +1,123 @@
package org.xbib.time.schedule;
import static org.junit.Assert.assertTrue;
import com.google.caliper.memory.ObjectGraphMeasurer;
import org.junit.Test;
public class CompareSizeToQuartzTest {
private static final CronExpression.Parser quartzLike = CronExpression.parser()
.withSecondsField(true)
.withOneBasedDayOfWeek(true)
.allowBothDayFields(false);
@Test
public void complex() throws Exception {
check("0/5 14,18,3-39,52 * ? JAN,MAR,SEP MON-FRI 2002-2010");
}
@Test
public void at_noon_every_day() throws Exception {
check("0 0 12 * * ?");
}
@Test
public void at_10_15am_every_day1() throws Exception {
check("0 15 10 ? * *");
}
@Test
public void at_10_15am_every_day2() throws Exception {
check("0 15 10 * * ?");
}
@Test
public void at_10_15am_every_day3() throws Exception {
check("0 15 10 * * ? *");
}
@Test
public void at_10_15am_every_day_in_2005() throws Exception {
check("0 15 10 * * ? 2005");
}
@Test
public void every_minute_of_2pm() throws Exception {
check("0 * 14 * * ?");
}
@Test
public void every_5_minutes_of_2pm() throws Exception {
check("0 0/5 14 * * ?");
}
@Test
public void every_5_minutes_of_2pm_and_6pm() throws Exception {
check("0 0/5 14,18 * * ?");
}
@Test
public void first_5_minutes_of_2pm() throws Exception {
check("0 0-5 14 * * ?");
}
@Test
public void at_2_10pm_and_2_44pm_every_wednesday_in_march() throws Exception {
check("0 10,44 14 ? 3 WED");
}
@Test
public void at_10_15am_every_weekday() throws Exception {
check("0 15 10 ? * MON-FRI");
}
@Test
public void at_10_15am_on_the_15th_of_every_month() throws Exception {
check("0 15 10 15 * ?");
}
@Test
public void at_10_15am_on_the_last_day_of_every_month() throws Exception {
check("0 15 10 L * ?");
}
@Test
public void at_10_15am_on_the_last_friday_of_every_month() throws Exception {
check("0 15 10 ? * 6L");
}
@Test
public void at_10_15am_on_the_last_friday_of_every_month_during_2002_through_2005() throws Exception {
check("0 15 10 ? * 6L 2002-2005");
}
@Test
public void at_10_15am_on_the_third_friday_of_every_month() throws Exception {
check("0 15 10 ? * 6#3");
}
@Test
public void at_noon_every_5_days_every_month_starting_on_the_first_day_of_the_month() throws Exception {
check("0 0 12 1/5 * ?");
}
@Test
public void november_11th_at_11_11am() throws Exception {
check("0 11 11 11 11 ?");
}
private void check(String expression) throws Exception {
CronExpression local = quartzLike.parse(expression);
org.quartz.CronExpression quartz = new org.quartz.CronExpression(expression);
long localSize = ObjectSizeCalculator.getObjectSize(local);
long quartzSize = ObjectSizeCalculator.getObjectSize(quartz);
assertTrue("We have more bytes", localSize < quartzSize);
ObjectGraphMeasurer.Footprint localFoot = ObjectGraphMeasurer.measure(local);
ObjectGraphMeasurer.Footprint quartzFoot = ObjectGraphMeasurer.measure(quartz);
assertTrue("We have more references", localFoot.getAllReferences() < quartzFoot.getAllReferences());
assertTrue("We have more non-null references", localFoot.getNonNullReferences() < quartzFoot.getNonNullReferences());
//assertTrue("We have more null references", localFoot.getNullReferences() < quartzFoot.getNullReferences());
assertTrue("We have more objects", localFoot.getObjects() < quartzFoot.getObjects());
assertTrue("We have more primitives", localFoot.getPrimitives().size() < quartzFoot.getPrimitives().size());
}
}

View file

@ -0,0 +1,66 @@
package org.xbib.time.schedule;
import static org.junit.Assert.assertTrue;
import com.google.common.base.Stopwatch;
import java.text.ParseException;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
public class CompareSpeedToQuartzTest extends CompareBehaviorToQuartzTest {
@Override
protected void check(final Iterable<ZonedDateTime> times) throws ParseException {
final Iterable<Date> dates = DateTimes.toDates(times);
final CronExpression local = quartzLike.parse(string);
final org.quartz.CronExpression quartz = new org.quartz.CronExpression(string);
final int trials = 25;
final Stopwatch clock = Stopwatch.createStarted();
for (int i = 0; i < trials; i++) {
for (ZonedDateTime time : times) {
local.matches(time);
}
}
final long localNano = clock.elapsed(TimeUnit.NANOSECONDS);
clock.reset().start();
for (int i = 0; i < trials; i++) {
for (Date date : dates) {
quartz.isSatisfiedBy(date);
}
}
final long quartzNano = clock.elapsed(TimeUnit.NANOSECONDS);
final boolean lessThanOrEqual = localNano <= quartzNano;
System.out.printf(
"%-80s %-60s local %8.2fms %6s Quartz %8.2fms\n",
nameOfTestMethod(),
string,
localNano / 1000000d,
(lessThanOrEqual ? "<=" : ">"),
quartzNano / 1000000d
);
assertTrue(
"We took longer for expression '" + string + "'; " + localNano + " > " + quartzNano,
lessThanOrEqual
);
}
private String nameOfTestMethod() {
try {
throw new Exception();
} catch (Exception e) {
String method = null;
Iterator<StackTraceElement> trace = Arrays.asList(e.getStackTrace()).iterator();
StackTraceElement element = trace.next();
while (getClass().getName().equals(element.getClassName())) {
element = trace.next();
}
String parentClassName = getClass().getSuperclass().getName();
while (element.getClassName().equals(parentClassName)) {
method = element.getMethodName();
element = trace.next();
}
return method;
}
}
}

View file

@ -0,0 +1,344 @@
package org.xbib.time.schedule;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.xbib.time.schedule.DateTimes.midnight;
import static org.xbib.time.schedule.DateTimes.nearestWeekday;
import static org.xbib.time.schedule.DateTimes.now;
import static org.xbib.time.schedule.DateTimes.nthOfMonth;
import static org.xbib.time.schedule.DateTimes.startOfHour;
import org.junit.Test;
import java.time.DayOfWeek;
import java.time.Month;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
public class CronExpressionTest {
private static final CronExpression.Parser withSecondsField = CronExpression.parser().withSecondsField(true);
private CronExpression expression;
@Test
public void testHashCode() {
assertEquals(CronExpression.daily().hashCode(), CronExpression.parse("@daily").hashCode());
assertEquals(CronExpression.daily().hashCode(), CronExpression.parse("@midnight").hashCode());
assertEquals(CronExpression.hourly().hashCode(), CronExpression.parse("@hourly").hashCode());
assertEquals(CronExpression.monthly().hashCode(), CronExpression.parse("@monthly").hashCode());
assertEquals(CronExpression.weekly().hashCode(), CronExpression.parse("@weekly").hashCode());
assertEquals(CronExpression.yearly().hashCode(), CronExpression.parse("@annually").hashCode());
assertEquals(CronExpression.yearly().hashCode(), CronExpression.parse("@yearly").hashCode());
assertEquals(
CronExpression.parse("0 0 ? * 5#3,2#2").hashCode(),
CronExpression.parse("0 0 ? * 5#3,2#2").hashCode());
assertEquals(
withSecondsField.parse("0/5 14,18,3-39,52 * ? JAN,MAR,SEP MON-FRI 2002-2010").hashCode(),
withSecondsField.parse("0/5 14,18,3-39,52 * ? JAN,MAR,SEP MON-FRI 2002-2010").hashCode());
}
@Test
public void testEquals() {
assertEquals(CronExpression.daily(), CronExpression.parse("@daily"));
assertEquals(CronExpression.daily(), CronExpression.parse("@midnight"));
assertEquals(CronExpression.hourly(), CronExpression.parse("@hourly"));
assertEquals(CronExpression.monthly(), CronExpression.parse("@monthly"));
assertEquals(CronExpression.weekly(), CronExpression.parse("@weekly"));
assertEquals(CronExpression.yearly(), CronExpression.parse("@annually"));
assertEquals(CronExpression.yearly(), CronExpression.parse("@yearly"));
assertEquals(
CronExpression.parse("0 0 ? * 5#3,2#2"),
CronExpression.parse("0 0 ? * 5#3,2#2"));
assertEquals(
withSecondsField.parse("0/5 14,18,3-39,52 * ? JAN,MAR,SEP MON-FRI 2002-2010"),
withSecondsField.parse("0/5 14,18,3-39,52 * ? JAN,MAR,SEP MON-FRI 2002-2010"));
}
@Test
public void illegalCharacter() {
try {
expression = CronExpression.parse("0 0 4X * *");
fail("Expected exception");
} catch (IllegalArgumentException e) {
assertEquals("Bad character 'X' at position 5 in string: 0 0 4X * *", e.getMessage());
}
}
@Test
public void disallowBothDayFields() {
try {
expression = CronExpression.parser().allowBothDayFields(false).parse("0 0 1 * 5L");
fail("Expected exception");
} catch (IllegalArgumentException e) {
assertEquals("Day of month and day of week may not both be specified", e.getMessage());
}
}
@Test
public void nearestWeekdayWithoutNumber() {
try {
expression = CronExpression.parse("0 0 W * *");
} catch (IllegalArgumentException e) {
assertEquals("Bad character 'W' in day of month field: W", e.getMessage());
}
}
@Test
public void nearestWeekdayOfMonth() {
expression = CronExpression.parse("0 0 5W * *");
List<ZonedDateTime> times = new ArrayList<>();
ZonedDateTime t = DateTimes.startOfYear();
int year = t.getYear();
do {
times.add(nearestWeekday(t.withDayOfMonth(5)));
t = t.plusMonths(1);
} while (year == t.getYear());
assertMatchesAll(times);
}
@Test
public void nearestFriday() {
ZonedDateTime t = now().truncatedTo(ChronoUnit.DAYS).with(DayOfWeek.SATURDAY);
expression = CronExpression.parse("0 0 " + t.getDayOfMonth() + "W * *");
assertMatches(t.minusDays(1));
}
@Test
public void nearestMonday() {
ZonedDateTime t = now().truncatedTo(ChronoUnit.DAYS).with(DayOfWeek.SUNDAY);
expression = CronExpression.parse("0 0 " + t.getDayOfMonth() + "W * *");
assertMatches(t.plusDays(1));
}
@Test
public void nonMatchingNth() {
expression = CronExpression.parse("0 0 ? * 2#2");
List<ZonedDateTime> times = new ArrayList<>();
ZonedDateTime t = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS).withDayOfYear(1);
int year = t.getYear();
while (t.getYear() == year) {
times.add(nthOfMonth(t, DayOfWeek.TUESDAY, 1));
t = t.plusMonths(1);
}
for (ZonedDateTime time : times) {
assertFalse(expression.matches(time));
}
}
@Test
public void multipleNth() {
expression = CronExpression.parse("0 0 ? * 5#3,2#2");
List<ZonedDateTime> times = new ArrayList<>();
ZonedDateTime t = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS).withDayOfYear(1);
int year = t.getYear();
while (t.getYear() == year) {
times.add(nthOfMonth(t, DayOfWeek.FRIDAY, 3));
times.add(nthOfMonth(t, DayOfWeek.TUESDAY, 2));
t = t.plusMonths(1);
}
assertMatchesAll(times);
}
@Test
public void thirdFriday() {
String string = "0 0 ? * 5#3";
List<ZonedDateTime> times = new ArrayList<>();
ZonedDateTime t = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS).withDayOfYear(1);
int year = t.getYear();
while (t.getYear() == year) {
times.add(nthOfMonth(t, DayOfWeek.FRIDAY, 3));
t = t.plusMonths(1);
}
expression = CronExpression.parse(string);
assertMatchesAll(times);
}
@Test
public void reboot() {
expression = CronExpression.parse("@reboot");
ZonedDateTime now = now();
assertTrue(expression.matches(now));
assertFalse(expression.matches(now));
}
@Test
public void minuteFullRangeExplicit() {
expression = CronExpression.parse("0-59 * * * * *");
ZonedDateTime time = startOfHour();
int hour = time.getHour();
do {
assertMatches(time);
time = time.plusMinutes(1);
} while (time.getHour() == hour);
}
@Test
public void minuteRestrictedRange() {
expression = CronExpression.parse("10-20 * * * * *");
int first = 10, last = 20;
ZonedDateTime time = startOfHour();
int hour = time.getHour();
do {
int minute = time.getMinute();
assertEquals(first <= minute && minute <= last, expression.matches(time));
time = time.plusMinutes(1);
} while (time.getHour() == hour);
}
@Test
public void minuteFullRangeMod() {
expression = CronExpression.parse("*/5 * * * * *");
ZonedDateTime time = startOfHour();
int hour = time.getHour();
do {
int minute = time.getMinute();
assertEquals(minute % 5 == 0, expression.matches(time));
time = time.plusMinutes(1);
} while (time.getHour() == hour);
}
@Test
public void minuteRestrictedRangeMod() {
expression = CronExpression.parse("10-20/5 * * * * *");
int first = 10, last = 20;
ZonedDateTime time = startOfHour();
int hour = time.getHour();
do {
int minute = time.getMinute();
assertEquals(first <= minute && minute <= last && minute % 5 == 0, expression.matches(time));
time = time.plusMinutes(1);
} while (time.getHour() == hour);
}
@Test
public void yearly() {
expression = CronExpression.yearly();
assertYearly();
}
@Test
public void monthly() {
expression = CronExpression.monthly();
assertMonthly();
}
@Test
public void weekly() {
expression = CronExpression.weekly();
assertWeekly();
}
@Test
public void daily() {
expression = CronExpression.daily();
assertDaily();
}
@Test
public void hourly() {
expression = CronExpression.hourly();
assertHourly();
}
@Test
public void yearlyKeyword() {
expression = CronExpression.parse("@yearly");
assertYearly();
}
@Test
public void annualKeyword() {
expression = CronExpression.parse("@annually");
assertYearly();
}
@Test
public void monthlyKeyword() {
expression = CronExpression.parse("@monthly");
assertMonthly();
}
@Test
public void weeklyKeyword() {
expression = CronExpression.parse("@weekly");
assertWeekly();
}
@Test
public void dailyKeyword() {
expression = CronExpression.parse("@daily");
assertDaily();
}
@Test
public void hourlyKeyword() {
expression = CronExpression.parse("@hourly");
assertHourly();
}
@Test
public void invalid() {
assertFalse(CronExpression.isValid(null));
assertFalse(CronExpression.isValid(""));
assertFalse(CronExpression.isValid("a"));
assertFalse(CronExpression.isValid("0 0 1 * X"));
assertFalse(CronExpression.isValid("0 0 1 * 1X"));
}
@Test
public void invalidDueToSecondsField() {
assertTrue(CronExpression.isValid("0 0 1 * 1"));
assertFalse(CronExpression.parser().allowBothDayFields(false).isValid("0 0 1 * 1"));
}
private void assertWeekly() {
for (int week = 1; week <= 52; week++) {
assertMatches(midnight().withDayOfYear(7 * week).with(DayOfWeek.SUNDAY));
}
}
private void assertDaily() {
for (int day = 1; day <= 365; day++) {
assertMatches(midnight().withDayOfYear(day));
}
}
private void assertMonthly() {
for (Month month : Month.values()) {
assertMatches(midnight().with(month).withDayOfMonth(1));
}
}
private void assertHourly() {
for (int day = 1; day <= 365; day++) {
for (int hour = 0; hour <= 23; hour++) {
assertMatches(midnight().withDayOfYear(day).withHour(hour));
}
}
}
private void assertYearly() {
assertMatches(midnight().withDayOfYear(1));
}
private static final String formatString = "m H d M E yyyy";
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatString);
private void assertMatchesAll(List<ZonedDateTime> times) {
for (ZonedDateTime time : times) {
assertMatches(time);
}
}
private void assertMatches(ZonedDateTime time) {
assertTrue(
time.format(formatter).toUpperCase() + " doesn't match expression: " + expression,
expression.matches(time)
);
}
}

View file

@ -0,0 +1,126 @@
package org.xbib.time.schedule;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.Multiset;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
public class CronScheduleTest {
private static final Logger logger = Logger.getLogger(CronScheduleTest.class.getName());
private CronSchedule<Void> schedule;
private ScheduledExecutorService executor;
@Before
public void before() {
executor = Executors.newScheduledThreadPool(1);
}
@Test
public void runMinutes() throws InterruptedException {
schedule = new CronSchedule<>(executor, 60000);
final AtomicBoolean run = new AtomicBoolean(false);
schedule.add("test", CronExpression.parser()
.parse("* * * * * *"),
() -> {
run.set(true);
return null;
});
assertFalse(run.get());
schedule.start();
Thread.sleep(TimeUnit.MINUTES.toMillis(2));
assertTrue(run.get());
logger.log(Level.INFO, schedule.toString());
}
@Test
public void runSeconds() throws Exception {
schedule = new CronSchedule<>(executor, 1000);
final AtomicBoolean run = new AtomicBoolean(false);
schedule.add("test", CronExpression.parser()
.withSecondsField(true).parse("* * * * * *"),
() -> {
run.set(true);
return null;
});
assertFalse(run.get());
schedule.start();
Thread.sleep(TimeUnit.SECONDS.toMillis(2));
assertTrue(run.get());
}
@Test
public void removeOne() throws Exception {
schedule = new CronSchedule<>(executor, 1000);
final Multiset<String> counts = HashMultiset.create();
Callable<Void> a = () -> {
counts.add("a");
return null;
};
Callable<Void> b = () -> {
counts.add("b");
return null;
};
CronExpression expression = CronExpression.parse("* * * * *");
schedule.add("1", expression, a);
schedule.add("2", expression, b);
runAndWait();
assertEquals(1, counts.count("a"));
assertEquals(1, counts.count("b"));
schedule.remove("1");
runAndWait();
assertEquals(1, counts.count("a"));
assertEquals(2, counts.count("b"));
}
@Test
public void removeAllForExpression() throws Exception {
schedule = new CronSchedule<>(executor, 1000);
final Multiset<String> counts = HashMultiset.create();
Callable<Void> a = () -> {
counts.add("a");
return null;
};
Callable<Void> b = () -> {
counts.add("b");
return null;
};
CronExpression expression = CronExpression.parse("* * * * *");
schedule.add("a", expression, a);
schedule.add("b", expression, b);
runAndWait();
assertEquals(1, counts.count("a"));
assertEquals(1, counts.count("b"));
schedule.remove("a");
schedule.remove("b");
runAndWait();
assertEquals(1, counts.count("a"));
assertEquals(1, counts.count("b"));
}
@After
public void after() throws IOException {
if (schedule != null) {
schedule.close();
}
}
private void runAndWait() throws InterruptedException {
schedule.run();
Thread.sleep(10);
}
}

View file

@ -0,0 +1,65 @@
package org.xbib.time.schedule;
import java.time.DayOfWeek;
import java.time.Month;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import java.util.Date;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
public class DateTimes {
static Iterable<Date> toDates(Iterable<ZonedDateTime> times) {
return StreamSupport.stream(times.spliterator(), false)
.map(input -> Date.from(input.toInstant())).collect(Collectors.toList());
}
static ZonedDateTime midnight() {
return now().truncatedTo(ChronoUnit.DAYS);
}
static ZonedDateTime startOfHour() {
return now().truncatedTo(ChronoUnit.HOURS);
}
public static ZonedDateTime now() {
return ZonedDateTime.now();
}
static ZonedDateTime lastOfMonth(ZonedDateTime t, DayOfWeek dayOfWeek) {
ZonedDateTime day = t.with(TemporalAdjusters.lastDayOfMonth()).with(dayOfWeek);
if (day.getMonth() != t.getMonth()) {
day = day.minusWeeks(1);
}
return day;
}
static ZonedDateTime nthOfMonth(ZonedDateTime t, DayOfWeek dayOfWeek, int desiredNumber) {
Month month = t.getMonth();
t = t.withDayOfMonth(1).with(dayOfWeek);
if (t.getMonth() != month) {
t = t.plusWeeks(1);
}
int number = 1;
while (number < desiredNumber && t.getMonth() == month) {
number++;
t = t.plusWeeks(1);
}
return t;
}
static ZonedDateTime nearestWeekday(ZonedDateTime t) {
if (t.getDayOfWeek() == DayOfWeek.SATURDAY) {
return t.minusDays(1);
} else if (t.getDayOfWeek() == DayOfWeek.SUNDAY) {
return t.plusDays(1);
}
return t;
}
static ZonedDateTime startOfYear() {
return midnight().withDayOfYear(1);
}
}

View file

@ -0,0 +1,36 @@
package org.xbib.time.schedule;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import java.time.DayOfWeek;
import java.time.ZonedDateTime;
import java.time.temporal.TemporalAdjusters;
public class DayOfMonthFieldTest {
@Test
public void last() {
DayOfMonthField field = parse("L");
assertTrue(field.matches(ZonedDateTime.now().with(TemporalAdjusters.lastDayOfMonth())));
}
@Test
public void nearestFriday() {
ZonedDateTime saturday = ZonedDateTime.now().with(DayOfWeek.SATURDAY);
ZonedDateTime friday = saturday.minusDays(1);
DayOfMonthField field = parse(saturday.getDayOfMonth() + "W");
assertTrue(field.matches(friday));
}
@Test
public void nearestMonday() {
ZonedDateTime sunday = ZonedDateTime.now().with(DayOfWeek.SUNDAY);
ZonedDateTime monday = sunday.plusDays(1);
DayOfMonthField field = parse(sunday.getDayOfMonth() + "W");
assertTrue(field.matches(monday));
}
private DayOfMonthField parse(String s) {
return DayOfMonthField.parse(new Tokens(s));
}
}

View file

@ -0,0 +1,28 @@
package org.xbib.time.schedule;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public class DayOfWeekFieldTest {
@Test
public void keywords() {
assertTrue(parse("MON", false).contains(1));
assertTrue(parse("TUE", false).contains(2));
assertTrue(parse("WED", false).contains(3));
assertTrue(parse("THU", false).contains(4));
assertTrue(parse("FRI", false).contains(5));
assertTrue(parse("SAT", false).contains(6));
assertTrue(parse("SUN", false).contains(7));
}
@Test
public void oneBased() {
assertTrue(parse("1", true).contains(0));
assertTrue(parse("2", true).contains(1));
}
private DayOfWeekField parse(String s, boolean oneBased) {
return DayOfWeekField.parse(new Tokens(s), oneBased);
}
}

View file

@ -0,0 +1,125 @@
package org.xbib.time.schedule;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import org.junit.Test;
public class DefaultFieldTest {
private DefaultField field;
@Test
public void emptyMonthField() {
parse("", 1, 12);
for (int month = 1; month <= 12; month++) {
assertFalse(field.contains(month));
}
}
@Test
public void backwardsRange() {
parse("2-1", 1, 2);
assertFalse(field.contains(0));
assertFalse(field.contains(1));
assertFalse(field.contains(2));
assertFalse(field.contains(3));
}
@Test
public void finalWildcardWins() {
parse("1-2,2,3,*", 1, 10);
assertContainsRange(1, 10);
}
@Test
public void initialWildcardWins() {
parse("*,1-2,2,3", 1, 10);
assertContainsRange(1, 10);
}
@Test
public void multipleRanges() {
parse("1-2,3-4", 1, 5);
assertContains(1, 2, 3, 4);
}
@Test
public void multipleNumbers() {
parse("1,2,3", 1, 5);
assertContains(1, 2, 3);
}
@Test
public void danglingRange() {
try {
parse("1-", 0, 0);
fail("Expected exception");
} catch (IllegalStateException e) {
assertEquals("Expected number", e.getMessage());
}
}
@Test
public void danglingSkip() {
try {
parse("1-2/", 0, 0);
fail("Expected exception");
} catch (IllegalStateException e) {
assertEquals("Expected number", e.getMessage());
}
}
@Test
public void range() {
parse("1-12", 1, 12);
assertContainsRange(1, 12);
}
@Test
public void wildcard() {
parse("*", 1, 12);
assertContainsRange(1, 12);
}
@Test
public void skipRangeWithImplicitEnd() {
parse("1/5", 1, 31);
assertContains(1, 6, 11, 16, 21, 26, 31);
}
@Test
public void oneBasedSkipRange() {
parse("1-31/5", 1, 31);
assertContains(1, 6, 11, 16, 21, 26, 31);
}
@Test
public void zeroBasedSkipRange() {
parse("0-20/5", 0, 59);
assertContains(0, 5, 10, 15, 20);
}
@Test
public void wildcardSkipRange() {
parse("*/5", 0, 20);
assertContains(0, 5, 10, 15, 20);
}
private void assertContains(int... numbers) {
for (int number : numbers) {
assertTrue(field.contains(number));
}
}
private void assertContainsRange(int first, int last) {
for (int number = first; number <= last; number++) {
assertContains(number);
}
}
private DefaultField parse(String s, int min, int max) {
return field = DefaultField.parse(new Tokens(s), min, max);
}
}

View file

@ -0,0 +1,42 @@
package org.xbib.time.schedule;
import com.google.common.collect.ForwardingSet;
import java.util.HashSet;
import java.util.Set;
public class Integers extends ForwardingSet<Integer> {
private final Set<Integer> delegate;
public Integers(int... integers) {
delegate = new HashSet<>();
with(integers);
}
@Override
protected Set<Integer> delegate() {
return delegate;
}
public Integers with(int... integers) {
for (int integer : integers) {
add(integer);
}
return this;
}
public Integers withRange(int start, int end) {
for (int i = start; i <= end; i++) {
add(i);
}
return this;
}
public Integers withRange(int start, int end, int mod) {
for (int i = start; i <= end; i++) {
if ((i - start) % mod == 0) {
add(i);
}
}
return this;
}
}

View file

@ -0,0 +1,79 @@
package org.xbib.time.schedule;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import org.junit.Before;
import org.junit.Test;
public class KeywordsTest {
private Keywords keywords;
@Before
public void before() {
keywords = new Keywords();
}
@Test
public void normalUse() {
keywords.put("AAA", 1);
keywords.put("BBB", 2);
assertEquals(1, keywords.get("AAABBB", 0, 3));
assertEquals(2, keywords.get("AAABBB", 3, 6));
}
@Test
public void getNotPresent() {
try {
assertEquals(-1, keywords.get("CCC", 0, 3));
fail("Expected exception");
} catch (IllegalArgumentException e) {
}
}
@Test
public void getWrongAlphabet() {
try {
keywords.get("aaa", 0, 3);
fail("Expected exception");
} catch (IllegalArgumentException e) {
}
}
@Test
public void getWrongLength() {
try {
keywords.get("aaa", 0, 1);
} catch (IllegalArgumentException e) {
}
}
@Test
public void putEmpty() {
try {
keywords.put("", 0);
fail("Expected exception");
} catch (StringIndexOutOfBoundsException e) {
assertEquals("String index out of range: 0", e.getMessage());
}
}
@Test
public void putWrongLength() {
try {
keywords.put("A", 0);
fail("Expected exception");
} catch (StringIndexOutOfBoundsException e) {
assertEquals("String index out of range: 1", e.getMessage());
}
}
@Test
public void putWrongAlphabet() {
try {
keywords.put("a", 0);
fail("Expected exception");
} catch (ArrayIndexOutOfBoundsException e) {
assertEquals("Index 32 out of bounds for length 26", e.getMessage());
}
}
}

View file

@ -0,0 +1,27 @@
package org.xbib.time.schedule;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public class MonthFieldTest {
@Test
public void keywords() {
assertTrue(parse("JAN").contains(1));
assertTrue(parse("FEB").contains(2));
assertTrue(parse("MAR").contains(3));
assertTrue(parse("APR").contains(4));
assertTrue(parse("MAY").contains(5));
assertTrue(parse("JUN").contains(6));
assertTrue(parse("JUL").contains(7));
assertTrue(parse("AUG").contains(8));
assertTrue(parse("SEP").contains(9));
assertTrue(parse("OCT").contains(10));
assertTrue(parse("NOV").contains(11));
assertTrue(parse("DEC").contains(12));
}
private DefaultField parse(String s) {
return MonthField.parse(new Tokens(s));
}
}

View file

@ -0,0 +1,79 @@
package org.xbib.time.schedule;
import org.junit.Test;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.logging.Level;
import java.util.logging.Logger;
public class NextExecutionTest {
private static final Logger logger = Logger.getLogger(NextExecutionTest.class.getName());
@Test
public void nextSecond() {
CronExpression expression = CronExpression.parser().withSecondsField(true)
.parse("0-59 * * * * *");
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime next = expression.nextExecution(now, now.plusMinutes(1));
logger.log(Level.INFO, now.toString());
logger.log(Level.INFO, next.toString());
}
@Test
public void nextHour() {
CronExpression expression = CronExpression.hourly();
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime next = expression.nextExecution(now, now.plusHours(2));
logger.log(Level.INFO, now.toString());
logger.log(Level.INFO, next.toString());
}
@Test
public void nextDay() {
CronExpression expression = CronExpression.daily();
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime next = expression.nextExecution(now, now.plusDays(2));
logger.log(Level.INFO, now.toString());
logger.log(Level.INFO, next.toString());
}
@Test
public void nextWeek() {
CronExpression expression = CronExpression.weekly();
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime next = expression.nextExecution(now, now.plusWeeks(2));
logger.log(Level.INFO, now.toString());
logger.log(Level.INFO, next.toString());
}
@Test
public void nextMonth() {
CronExpression expression = CronExpression.monthly();
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime next = expression.nextExecution(now, now.plusMonths(2));
logger.log(Level.INFO, now.toString());
logger.log(Level.INFO, next.toString());
}
@Test
public void nextFractionMinute() {
CronExpression expression = CronExpression.parse("10-20/5 * * * * *");
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime next = expression.nextExecution(now, now.plusHours(2));
logger.log(Level.INFO, now.toString());
logger.log(Level.INFO, next.toString());
}
@Test
public void nextMultipleNth() {
CronExpression expression = CronExpression.parser()
.withSecondsField(true)
.parse("0/5 14,18,3-39,52 * ? JAN,MAR,SEP MON-FRI 2002-2010");
ZonedDateTime begin = ZonedDateTime.of(2002, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
logger.log(Level.INFO, expression.toString());
logger.log(Level.INFO, begin.toString());
ZonedDateTime next = expression.nextExecution(begin, begin.plusYears(1));
logger.log(Level.INFO, next.toString());
}
}

View file

@ -0,0 +1,436 @@
package org.xbib.time.schedule;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Sets;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryPoolMXBean;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
/**
* Contains utility methods for calculating the memory usage of objects. It
* only works on the HotSpot JVM, and infers the actual memory layout (32 bit
* vs. 64 bit word size, compressed object pointers vs. uncompressed) from
* best available indicators. It can reliably detect a 32 bit vs. 64 bit JVM.
* It can only make an educated guess at whether compressed OOPs are used,
* though; specifically, it knows what the JVM's default choice of OOP
* compression would be based on HotSpot version and maximum heap sizes, but if
* the choice is explicitly overridden with the <tt>-XX:{+|-}UseCompressedOops</tt> command line
* switch, it can not detect
* this fact and will report incorrect sizes, as it will presume the default JVM
* behavior.
*/
public class ObjectSizeCalculator {
/**
* Describes constant memory overheads for various constructs in a JVM implementation.
*/
public interface MemoryLayoutSpecification {
/**
* Returns the fixed overhead of an array of any type or length in this JVM.
*
* @return the fixed overhead of an array.
*/
int getArrayHeaderSize();
/**
* Returns the fixed overhead of for any {@link Object} subclass in this JVM.
*
* @return the fixed overhead of any object.
*/
int getObjectHeaderSize();
/**
* Returns the quantum field size for a field owned by an object in this JVM.
*
* @return the quantum field size for an object.
*/
int getObjectPadding();
/**
* Returns the fixed size of an object reference in this JVM.
*
* @return the size of all object references.
*/
int getReferenceSize();
/**
* Returns the quantum field size for a field owned by one of an object's ancestor superclasses
* in this JVM.
*
* @return the quantum field size for a superclass field.
*/
int getSuperclassFieldPadding();
}
private static class CurrentLayout {
private static final MemoryLayoutSpecification SPEC =
getEffectiveMemoryLayoutSpecification();
}
/**
* Given an object, returns the total allocated size, in bytes, of the object
* and all other objects reachable from it. Attempts to to detect the current JVM memory layout,
* but may fail with {@link UnsupportedOperationException};
*
* @param obj the object; can be null. Passing in a {@link Class} object doesn't do
* anything special, it measures the size of all objects
* reachable through it (which will include its class loader, and by
* extension, all other Class objects loaded by
* the same loader, and all the parent class loaders). It doesn't provide the
* size of the static fields in the JVM class that the Class object
* represents.
* @return the total allocated size of the object and all other objects it
* retains.
* @throws UnsupportedOperationException if the current vm memory layout cannot be detected.
*/
public static long getObjectSize(Object obj) throws UnsupportedOperationException {
return obj == null ? 0 : new ObjectSizeCalculator(CurrentLayout.SPEC).calculateObjectSize(obj);
}
// Fixed object header size for arrays.
private final int arrayHeaderSize;
// Fixed object header size for non-array objects.
private final int objectHeaderSize;
// Padding for the object size - if the object size is not an exact multiple
// of this, it is padded to the next multiple.
private final int objectPadding;
// Size of reference (pointer) fields.
private final int referenceSize;
// Padding for the fields of superclass before fields of subclasses are
// added.
private final int superclassFieldPadding;
private final LoadingCache<Class<?>, ClassSizeInfo> classSizeInfos =
CacheBuilder.newBuilder().build(new CacheLoader<Class<?>, ClassSizeInfo>() {
public ClassSizeInfo load(Class<?> clazz) {
return new ClassSizeInfo(clazz);
}
});
private final Set<Object> alreadyVisited = Sets.newIdentityHashSet();
private final Deque<Object> pending = new ArrayDeque<Object>(16 * 1024);
private long size;
/**
* Creates an object size calculator that can calculate object sizes for a given
* {@code memoryLayoutSpecification}.
*
* @param memoryLayoutSpecification a description of the JVM memory layout.
*/
public ObjectSizeCalculator(MemoryLayoutSpecification memoryLayoutSpecification) {
Preconditions.checkNotNull(memoryLayoutSpecification);
arrayHeaderSize = memoryLayoutSpecification.getArrayHeaderSize();
objectHeaderSize = memoryLayoutSpecification.getObjectHeaderSize();
objectPadding = memoryLayoutSpecification.getObjectPadding();
referenceSize = memoryLayoutSpecification.getReferenceSize();
superclassFieldPadding = memoryLayoutSpecification.getSuperclassFieldPadding();
}
/**
* Given an object, returns the total allocated size, in bytes, of the object
* and all other objects reachable from it.
*
* @param obj the object; can be null. Passing in a {@link Class} object doesn't do
* anything special, it measures the size of all objects
* reachable through it (which will include its class loader, and by
* extension, all other Class objects loaded by
* the same loader, and all the parent class loaders). It doesn't provide the
* size of the static fields in the JVM class that the Class object
* represents.
* @return the total allocated size of the object and all other objects it
* retains.
*/
public synchronized long calculateObjectSize(Object obj) {
// Breadth-first traversal instead of naive depth-first with recursive
// implementation, so we don't blow the stack traversing long linked lists.
try {
for (; ; ) {
visit(obj);
if (pending.isEmpty()) {
return size;
}
obj = pending.removeFirst();
}
} finally {
alreadyVisited.clear();
pending.clear();
size = 0;
}
}
private void visit(Object obj) {
if (alreadyVisited.contains(obj)) {
return;
}
final Class<?> clazz = obj.getClass();
if (clazz == ArrayElementsVisitor.class) {
((ArrayElementsVisitor) obj).visit(this);
} else {
alreadyVisited.add(obj);
if (clazz.isArray()) {
visitArray(obj);
} else {
classSizeInfos.getUnchecked(clazz).visit(obj, this);
}
}
}
private void visitArray(Object array) {
final Class<?> componentType = array.getClass().getComponentType();
final int length = Array.getLength(array);
if (componentType.isPrimitive()) {
increaseByArraySize(length, getPrimitiveFieldSize(componentType));
} else {
increaseByArraySize(length, referenceSize);
// If we didn't use an ArrayElementsVisitor, we would be enqueueing every
// element of the array here instead. For large arrays, it would
// tremendously enlarge the queue. In essence, we're compressing it into
// a small command object instead. This is different than immediately
// visiting the elements, as their visiting is scheduled for the end of
// the current queue.
switch (length) {
case 0: {
break;
}
case 1: {
enqueue(Array.get(array, 0));
break;
}
default: {
enqueue(new ArrayElementsVisitor((Object[]) array));
}
}
}
}
private void increaseByArraySize(int length, long elementSize) {
increaseSize(roundTo(arrayHeaderSize + length * elementSize, objectPadding));
}
private static class ArrayElementsVisitor {
private final Object[] array;
ArrayElementsVisitor(Object[] array) {
this.array = array;
}
public void visit(ObjectSizeCalculator calc) {
for (Object elem : array) {
if (elem != null) {
calc.visit(elem);
}
}
}
}
void enqueue(Object obj) {
if (obj != null) {
pending.addLast(obj);
}
}
void increaseSize(long objectSize) {
size += objectSize;
}
@VisibleForTesting
static long roundTo(long x, int multiple) {
return ((x + multiple - 1) / multiple) * multiple;
}
private class ClassSizeInfo {
// Padded fields + header size
private final long objectSize;
// Only the fields size - used to calculate the subclasses' memory
// footprint.
private final long fieldsSize;
private final Field[] referenceFields;
public ClassSizeInfo(Class<?> clazz) {
long fieldsSize = 0;
final List<Field> referenceFields = new LinkedList<Field>();
for (Field f : clazz.getDeclaredFields()) {
if (Modifier.isStatic(f.getModifiers())) {
continue;
}
final Class<?> type = f.getType();
if (type.isPrimitive()) {
fieldsSize += getPrimitiveFieldSize(type);
} else {
f.setAccessible(true);
referenceFields.add(f);
fieldsSize += referenceSize;
}
}
final Class<?> superClass = clazz.getSuperclass();
if (superClass != null) {
final ClassSizeInfo superClassInfo = classSizeInfos.getUnchecked(superClass);
fieldsSize += roundTo(superClassInfo.fieldsSize, superclassFieldPadding);
referenceFields.addAll(Arrays.asList(superClassInfo.referenceFields));
}
this.fieldsSize = fieldsSize;
this.objectSize = roundTo(objectHeaderSize + fieldsSize, objectPadding);
this.referenceFields = referenceFields.toArray(
new Field[referenceFields.size()]);
}
void visit(Object obj, ObjectSizeCalculator calc) {
calc.increaseSize(objectSize);
enqueueReferencedObjects(obj, calc);
}
public void enqueueReferencedObjects(Object obj, ObjectSizeCalculator calc) {
for (Field f : referenceFields) {
try {
calc.enqueue(f.get(obj));
} catch (IllegalAccessException e) {
final AssertionError ae = new AssertionError(
"Unexpected denial of access to " + f, e);
throw ae;
}
}
}
}
private static long getPrimitiveFieldSize(Class<?> type) {
if (type == boolean.class || type == byte.class) {
return 1;
}
if (type == char.class || type == short.class) {
return 2;
}
if (type == int.class || type == float.class) {
return 4;
}
if (type == long.class || type == double.class) {
return 8;
}
throw new AssertionError("Encountered unexpected primitive type " +
type.getName());
}
@VisibleForTesting
static MemoryLayoutSpecification getEffectiveMemoryLayoutSpecification() {
final String vmName = System.getProperty("java.vm.name");
if (vmName == null || !(vmName.startsWith("Java HotSpot(TM) ") || vmName.startsWith("OpenJDK"))) {
throw new UnsupportedOperationException(
"ObjectSizeCalculator only supported on HotSpot VM");
}
final String dataModel = System.getProperty("sun.arch.data.model");
if ("32".equals(dataModel)) {
// Running with 32-bit data model
return new MemoryLayoutSpecification() {
@Override
public int getArrayHeaderSize() {
return 12;
}
@Override
public int getObjectHeaderSize() {
return 8;
}
@Override
public int getObjectPadding() {
return 8;
}
@Override
public int getReferenceSize() {
return 4;
}
@Override
public int getSuperclassFieldPadding() {
return 4;
}
};
} else if (!"64".equals(dataModel)) {
throw new UnsupportedOperationException("Unrecognized value '" +
dataModel + "' of sun.arch.data.model system property");
}
final String strVmVersion = System.getProperty("java.vm.version");
final int vmVersion = Integer.parseInt(strVmVersion.substring(0,
strVmVersion.indexOf('.')));
if (vmVersion >= 17) {
long maxMemory = 0;
for (MemoryPoolMXBean mp : ManagementFactory.getMemoryPoolMXBeans()) {
maxMemory += mp.getUsage().getMax();
}
if (maxMemory < 30L * 1024 * 1024 * 1024) {
// HotSpot 17.0 and above use compressed OOPs below 30GB of RAM total
// for all memory pools (yes, including code cache).
return new MemoryLayoutSpecification() {
@Override
public int getArrayHeaderSize() {
return 16;
}
@Override
public int getObjectHeaderSize() {
return 12;
}
@Override
public int getObjectPadding() {
return 8;
}
@Override
public int getReferenceSize() {
return 4;
}
@Override
public int getSuperclassFieldPadding() {
return 4;
}
};
}
}
// In other cases, it's a 64-bit uncompressed OOPs object model
return new MemoryLayoutSpecification() {
@Override
public int getArrayHeaderSize() {
return 24;
}
@Override
public int getObjectHeaderSize() {
return 16;
}
@Override
public int getObjectPadding() {
return 8;
}
@Override
public int getReferenceSize() {
return 8;
}
@Override
public int getSuperclassFieldPadding() {
return 8;
}
};
}
}

View file

@ -0,0 +1,38 @@
package org.xbib.time.schedule;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
/**
* Examples used in the Readme file.
*/
public class ReadmeTest {
@Test
public void normal() {
ZonedDateTime time = ZonedDateTime.now().withDayOfYear(1).truncatedTo(ChronoUnit.DAYS);
assertTrue(CronExpression.parse("0 0 1 1 *").matches(time));
assertTrue(CronExpression.parse("@yearly").matches(time));
assertTrue(CronExpression.parse("@annually").matches(time));
assertTrue(CronExpression.yearly().matches(time));
}
@Test
public void quartzLike() {
CronExpression expression = CronExpression.parser()
.withSecondsField(true)
.withOneBasedDayOfWeek(true)
.allowBothDayFields(false)
.parse("0 15 10 L * ?");
ZonedDateTime time = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS)
.withYear(2013)
.withMonth(1)
.withDayOfMonth(31)
.withHour(10)
.withMinute(15);
assertTrue(expression.matches(time));
}
}

View file

@ -0,0 +1,102 @@
package org.xbib.time.schedule;
import com.google.common.collect.ImmutableSortedSet;
import java.time.DayOfWeek;
import java.time.Month;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import java.util.Iterator;
import java.util.NavigableSet;
public class Times {
final Integers
seconds,
minutes,
hours,
months,
daysOfWeek,
years,
daysOfMonth;
Times() {
seconds = new Integers();
minutes = new Integers();
hours = new Integers();
months = new Integers();
daysOfWeek = new Integers();
years = new Integers();
daysOfMonth = new Integers();
}
NavigableSet<ZonedDateTime> dateTimes() {
if (seconds.isEmpty()) {
seconds.withRange(0, 1);
}
if (minutes.isEmpty()) {
minutes.withRange(0, 1);
}
if (hours.isEmpty()) {
hours.withRange(0, 1);
}
if (months.isEmpty()) {
months.withRange(1, 2);
}
if (years.isEmpty()) {
int thisYear = ZonedDateTime.now().getYear();
years.withRange(thisYear, thisYear + 1);
}
ImmutableSortedSet.Builder<ZonedDateTime> builder = ImmutableSortedSet.naturalOrder();
for (int second : seconds) {
for (int minute : minutes) {
for (int hour : hours) {
for (int month : months) {
for (int year : years) {
ZonedDateTime base = ZonedDateTime.now()
.truncatedTo(ChronoUnit.DAYS)
.withSecond(second)
.withMinute(minute)
.withHour(hour)
.withMonth(month)
.withDayOfMonth(1)
.withYear(year);
if (!daysOfWeek.isEmpty() && !daysOfMonth.isEmpty()) {
addDaysOfWeek(builder, base);
addDaysOfMonth(builder, base);
} else if (!daysOfWeek.isEmpty()) {
addDaysOfWeek(builder, base);
} else if (!daysOfMonth.isEmpty()) {
addDaysOfMonth(builder, base);
} else {
builder.add(base);
}
}
}
}
}
}
return builder.build();
}
private void addDaysOfWeek(ImmutableSortedSet.Builder<ZonedDateTime> builder, ZonedDateTime base) {
Month month = base.getMonth();
Iterator<Integer> iterator = daysOfWeek.iterator();
base = base.with(DayOfWeek.of(iterator.next()));
if (base.getMonth() != month) {
base = base.plusWeeks(1);
}
do {
builder.add(base);
base = base.plusWeeks(1);
} while (base.getMonth() == month);
}
private void addDaysOfMonth(ImmutableSortedSet.Builder<ZonedDateTime> builder, ZonedDateTime base) {
for (int day : daysOfMonth) {
if (day <= base.with(TemporalAdjusters.lastDayOfMonth()).getDayOfMonth()) {
builder.add(base.withDayOfMonth(day));
}
}
}
}

View file

@ -0,0 +1,204 @@
package org.xbib.time.schedule;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.fail;
import org.junit.Test;
public class TokensTest {
private Tokens tokens;
@Test
public void empty() {
tokenize("");
assertFalse(tokens.hasNext());
assertEndOfInput();
}
@Test
public void end() {
tokenize("1");
assertNextIsNumber(1);
assertFalse(tokens.hasNext());
assertEndOfInput();
}
@Test
public void offset() {
tokenize("5,6");
tokens.offset(5);
assertNextIsNumber(0);
assertNextIs(Token.VALUE_SEPARATOR);
assertNextIsNumber(1);
assertEndOfInput();
}
@Test
public void resetClearsOffset() {
tokenize("2,2");
tokens.offset(1);
assertNextIsNumber(1);
assertNextIs(Token.VALUE_SEPARATOR);
tokens.reset();
assertNextIsNumber(2);
}
@Test
public void resetClearsKeywords() {
tokenize("FRI,FRI");
tokens.keywords(DayOfWeekField.Builder.KEYWORDS);
assertNextIsNumber(5);
assertNextIs(Token.VALUE_SEPARATOR);
tokens.reset();
try {
tokens.next();
fail("Expected exception");
} catch (IllegalArgumentException e) {
assertEquals("Bad keyword 'FRI' at position 4 in string: FRI,FRI", e.getMessage());
}
}
@Test
public void matchOne() {
tokenize("?");
assertNextIs(Token.MATCH_ONE);
assertEndOfInput();
}
@Test
public void matchAll() {
tokenize("*");
assertNextIs(Token.MATCH_ALL);
assertEndOfInput();
}
@Test
public void skip() {
tokenize("/");
assertNextIs(Token.SKIP);
assertEndOfInput();
}
@Test
public void range() {
tokenize("-");
assertNextIs(Token.RANGE);
assertEndOfInput();
}
@Test
public void last() {
tokenize("1L");
assertNextIsNumber(1);
assertNextIs(Token.LAST);
assertEndOfInput();
}
@Test
public void lastAlone() {
tokenize("L");
assertNextIs(Token.LAST);
assertEndOfInput();
}
@Test
public void weekday() {
tokenize("1W");
assertNextIsNumber(1);
assertNextIs(Token.WEEKDAY);
assertEndOfInput();
}
@Test
public void nth() {
tokenize("1#2");
assertNextIsNumber(1);
assertNextIs(Token.NTH);
assertNextIsNumber(2);
assertEndOfInput();
}
@Test
public void multipleWhitespaceCharacters() {
tokenize(" \t \t \t \t ");
assertEquals(Token.FIELD_SEPARATOR, tokens.next());
assertEndOfInput();
}
@Test
public void keywordRange() {
tokenize("MON-FRI");
tokens.keywords(DayOfWeekField.Builder.KEYWORDS);
assertEquals(Token.NUMBER, tokens.next());
assertEquals(1, tokens.number());
assertEquals(Token.RANGE, tokens.next());
assertEquals(Token.NUMBER, tokens.next());
assertEquals(5, tokens.number());
assertEndOfInput();
}
@Test
public void badCharacter() {
tokenize("5%");
assertEquals(Token.NUMBER, tokens.next());
try {
tokens.next();
fail("Expected exception");
} catch (IllegalArgumentException e) {
assertEquals("Bad character '%' at position 1 in string: 5%", e.getMessage());
}
}
@Test
public void badLetter() {
tokenize("1F");
assertEquals(Token.NUMBER, tokens.next());
try {
tokens.next();
fail("Expected exception");
} catch (IllegalArgumentException e) {
assertEquals("Bad character 'F' at position 1 in string: 1F", e.getMessage());
}
}
@Test
public void badKeywordOfValidLength() {
tokenize("ABC");
tokens.keywords(DayOfWeekField.Builder.KEYWORDS);
try {
tokens.next();
fail("Expected exception");
} catch (IllegalArgumentException e) {
assertEquals("Bad keyword 'ABC' at position 0 in string: ABC", e.getMessage());
}
}
@Test
public void badKeywordOfInvalidLength() {
tokenize("AB");
tokens.keywords(DayOfWeekField.Builder.KEYWORDS);
try {
tokens.next();
fail("Expected exception");
} catch (IllegalArgumentException e) {
assertEquals("Bad keyword 'AB' at position 0 in string: AB", e.getMessage());
}
}
private void assertEndOfInput() {
assertNextIs(Token.END_OF_INPUT);
}
private void assertNextIsNumber(int expected) {
assertNextIs(Token.NUMBER);
assertEquals(expected, tokens.number());
}
private void assertNextIs(Token expected) {
assertEquals(expected, tokens.next());
}
private void tokenize(String s) {
tokens = new Tokens(s);
}
}

View file

@ -0,0 +1,53 @@
package org.xbib.time.schedule;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.xbib.time.schedule.DateTimes.nthOfMonth;
import org.junit.Test;
import java.text.ParseException;
import java.time.DayOfWeek;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class WhatQuartzDoesNotSupport {
@Test
public void multipleNthDayOfWeek() {
try {
org.quartz.CronExpression quartz = new org.quartz.CronExpression("0 0 0 ? * 6#3,4#1,3#2");
List<ZonedDateTime> times = new ArrayList<>();
ZonedDateTime t = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS).withDayOfYear(1);
int year = t.getYear();
while (t.getYear() == year) {
times.add(nthOfMonth(t, DayOfWeek.FRIDAY, 3));
times.add(nthOfMonth(t, DayOfWeek.TUESDAY, 2));
t = t.plusMonths(1);
}
for (ZonedDateTime time : times) {
boolean satisfied = quartz.isSatisfiedBy(Date.from(time.toInstant()));
if (time.getDayOfWeek() == DayOfWeek.TUESDAY) {
// Earlier versions of Quartz only picked up the last one
assertTrue(satisfied);
} else {
assertFalse(satisfied);
}
}
} catch (ParseException e) {
assertEquals("Support for specifying multiple \"nth\" days is not implemented.", e.getMessage());
}
}
@Test
public void multipleLastDayOfWeek() throws Exception {
try {
new org.quartz.CronExpression("0 0 0 ? * 6L,4L,3L");
fail("Expected exception");
} catch (ParseException e) {
assertEquals("Support for specifying 'L' with other days of the week is not implemented", e.getMessage());
}
}
}

View file

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="OFF">
<appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="[%d{ABSOLUTE}][%-5p][%-25c][%t] %m%n"/>
</Console>
</appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console" />
</Root>
</Loggers>
</configuration>