add cron expressions, update to OpenJDK 11, gradle 5.6
This commit is contained in:
parent
74d51599b1
commit
eb0b999f4e
79 changed files with 5286 additions and 1422 deletions
|
@ -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
|
||||
|
|
144
build.gradle
144
build.gradle
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
}
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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/"
|
||||
}
|
||||
}
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
6
gradle/wrapper/gradle-wrapper.properties
vendored
6
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -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
|
||||
|
|
28
gradlew
vendored
28
gradlew
vendored
|
@ -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,16 +44,16 @@ 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"
|
||||
|
||||
warn ( ) {
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die ( ) {
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
|
@ -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"`
|
||||
|
@ -155,7 +171,7 @@ if $cygwin ; then
|
|||
fi
|
||||
|
||||
# Escape application args
|
||||
save ( ) {
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
|
|
18
gradlew.bat
vendored
18
gradlew.bat
vendored
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
130
src/main/java/org/xbib/time/schedule/CronExpression.java
Normal file
130
src/main/java/org/xbib/time/schedule/CronExpression.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
82
src/main/java/org/xbib/time/schedule/CronSchedule.java
Normal file
82
src/main/java/org/xbib/time/schedule/CronSchedule.java
Normal 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();
|
||||
}
|
||||
}
|
122
src/main/java/org/xbib/time/schedule/DayOfMonthField.java
Normal file
122
src/main/java/org/xbib/time/schedule/DayOfMonthField.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
173
src/main/java/org/xbib/time/schedule/DayOfWeekField.java
Normal file
173
src/main/java/org/xbib/time/schedule/DayOfWeekField.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
228
src/main/java/org/xbib/time/schedule/DefaultCronExpression.java
Normal file
228
src/main/java/org/xbib/time/schedule/DefaultCronExpression.java
Normal 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 +
|
||||
"]";
|
||||
}
|
||||
}
|
177
src/main/java/org/xbib/time/schedule/DefaultField.java
Normal file
177
src/main/java/org/xbib/time/schedule/DefaultField.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
57
src/main/java/org/xbib/time/schedule/Entry.java
Normal file
57
src/main/java/org/xbib/time/schedule/Entry.java
Normal 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 + "]";
|
||||
}
|
||||
}
|
42
src/main/java/org/xbib/time/schedule/Keywords.java
Normal file
42
src/main/java/org/xbib/time/schedule/Keywords.java
Normal 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';
|
||||
}
|
||||
}
|
23
src/main/java/org/xbib/time/schedule/MatchAllField.java
Normal file
23
src/main/java/org/xbib/time/schedule/MatchAllField.java
Normal 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;
|
||||
}
|
||||
}
|
48
src/main/java/org/xbib/time/schedule/MonthField.java
Normal file
48
src/main/java/org/xbib/time/schedule/MonthField.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
12
src/main/java/org/xbib/time/schedule/TimeField.java
Normal file
12
src/main/java/org/xbib/time/schedule/TimeField.java
Normal 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();
|
||||
}
|
15
src/main/java/org/xbib/time/schedule/Token.java
Normal file
15
src/main/java/org/xbib/time/schedule/Token.java
Normal 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
|
||||
}
|
189
src/main/java/org/xbib/time/schedule/Tokens.java
Normal file
189
src/main/java/org/xbib/time/schedule/Tokens.java
Normal 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';
|
||||
}
|
||||
}
|
4
src/main/java/org/xbib/time/schedule/package-info.java
Normal file
4
src/main/java/org/xbib/time/schedule/package-info.java
Normal file
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
* Schedule jobs (like cron).
|
||||
*/
|
||||
package org.xbib.time.schedule;
|
126
src/main/java/org/xbib/time/util/AbstractMultiMap.java
Normal file
126
src/main/java/org/xbib/time/util/AbstractMultiMap.java
Normal 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();
|
||||
}
|
29
src/main/java/org/xbib/time/util/LinkedHashSetMultiMap.java
Normal file
29
src/main/java/org/xbib/time/util/LinkedHashSetMultiMap.java
Normal 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<>();
|
||||
}
|
||||
}
|
38
src/main/java/org/xbib/time/util/MultiMap.java
Normal file
38
src/main/java/org/xbib/time/util/MultiMap.java
Normal 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();
|
||||
}
|
31
src/main/java/org/xbib/time/util/TreeMultiMap.java
Normal file
31
src/main/java/org/xbib/time/util/TreeMultiMap.java
Normal 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<>();
|
||||
}
|
||||
}
|
4
src/main/java/org/xbib/time/util/package-info.java
Normal file
4
src/main/java/org/xbib/time/util/package-info.java
Normal file
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package org.xbib.time.util;
|
|
@ -10,7 +10,7 @@ import java.util.Locale;
|
|||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class FormatterTest {
|
||||
public class DateTimeFormatterTest {
|
||||
|
||||
@Test
|
||||
public void testLocalDate() {
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
344
src/test/java/org/xbib/time/schedule/CronExpressionTest.java
Normal file
344
src/test/java/org/xbib/time/schedule/CronExpressionTest.java
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
126
src/test/java/org/xbib/time/schedule/CronScheduleTest.java
Normal file
126
src/test/java/org/xbib/time/schedule/CronScheduleTest.java
Normal 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);
|
||||
}
|
||||
}
|
65
src/test/java/org/xbib/time/schedule/DateTimes.java
Normal file
65
src/test/java/org/xbib/time/schedule/DateTimes.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
28
src/test/java/org/xbib/time/schedule/DayOfWeekFieldTest.java
Normal file
28
src/test/java/org/xbib/time/schedule/DayOfWeekFieldTest.java
Normal 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);
|
||||
}
|
||||
}
|
125
src/test/java/org/xbib/time/schedule/DefaultFieldTest.java
Normal file
125
src/test/java/org/xbib/time/schedule/DefaultFieldTest.java
Normal 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);
|
||||
}
|
||||
}
|
42
src/test/java/org/xbib/time/schedule/Integers.java
Normal file
42
src/test/java/org/xbib/time/schedule/Integers.java
Normal 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;
|
||||
}
|
||||
}
|
79
src/test/java/org/xbib/time/schedule/KeywordsTest.java
Normal file
79
src/test/java/org/xbib/time/schedule/KeywordsTest.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
27
src/test/java/org/xbib/time/schedule/MonthFieldTest.java
Normal file
27
src/test/java/org/xbib/time/schedule/MonthFieldTest.java
Normal 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));
|
||||
}
|
||||
}
|
79
src/test/java/org/xbib/time/schedule/NextExecutionTest.java
Normal file
79
src/test/java/org/xbib/time/schedule/NextExecutionTest.java
Normal 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());
|
||||
}
|
||||
}
|
436
src/test/java/org/xbib/time/schedule/ObjectSizeCalculator.java
Normal file
436
src/test/java/org/xbib/time/schedule/ObjectSizeCalculator.java
Normal 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
38
src/test/java/org/xbib/time/schedule/ReadmeTest.java
Normal file
38
src/test/java/org/xbib/time/schedule/ReadmeTest.java
Normal 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));
|
||||
}
|
||||
|
||||
}
|
102
src/test/java/org/xbib/time/schedule/Times.java
Normal file
102
src/test/java/org/xbib/time/schedule/Times.java
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
204
src/test/java/org/xbib/time/schedule/TokensTest.java
Normal file
204
src/test/java/org/xbib/time/schedule/TokensTest.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
Loading…
Reference in a new issue