diff --git a/.classpath b/.classpath
new file mode 100644
index 0000000..4cab939
--- /dev/null
+++ b/.classpath
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/.gitignore b/.gitignore
index 32858aa..8b6f86a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,4 @@
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
+/.exploded/
diff --git a/.project b/.project
new file mode 100644
index 0000000..75d6b6a
--- /dev/null
+++ b/.project
@@ -0,0 +1,27 @@
+
+
+ Chime
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ com.redhat.ceylon.eclipse.ui.ceylonBuilder
+
+
+ systemRepo
+
+
+
+
+
+
+ com.redhat.ceylon.eclipse.ui.ceylonNature
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/README.md b/README.md
index 48dd7f4..4b0670a 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,20 @@
-# Chime
-Time scheduler for Vert.x
+## Chime
+
+_Chime_ is time scheduler which works on _Vert.x_ event bus and provides:
+
+* scheduling with _cron-style_ and _interval_ timers
+* applying time zones available on _JVM_
+
+>Runs with Ceylon 1.2.0 and Vert.x 3.2.1
+
+
+## Dependences
+
+* ceylon.language/1.2.0
+* ceylon.time/1.2.0
+* io.vertx.ceylon.core/3.2.1
+
+
+## Documentation
+
+
diff --git a/examples/herd/examples/schedule/chime/module.ceylon b/examples/herd/examples/schedule/chime/module.ceylon
new file mode 100644
index 0000000..d539bb6
--- /dev/null
+++ b/examples/herd/examples/schedule/chime/module.ceylon
@@ -0,0 +1,4 @@
+native("jvm")
+module herd.examples.schedule.chime "0.1.0" {
+ shared import io.vertx.ceylon.core "3.2.1";
+}
diff --git a/examples/herd/examples/schedule/chime/package.ceylon b/examples/herd/examples/schedule/chime/package.ceylon
new file mode 100644
index 0000000..781d781
--- /dev/null
+++ b/examples/herd/examples/schedule/chime/package.ceylon
@@ -0,0 +1 @@
+shared package herd.examples.schedule.chime;
diff --git a/examples/herd/examples/schedule/chime/run.ceylon b/examples/herd/examples/schedule/chime/run.ceylon
new file mode 100644
index 0000000..c8d9267
--- /dev/null
+++ b/examples/herd/examples/schedule/chime/run.ceylon
@@ -0,0 +1,111 @@
+import io.vertx.ceylon.core.eventbus {
+
+ EventBus,
+ Message
+}
+import ceylon.json {
+
+ JSON=Object
+}
+import io.vertx.ceylon.core {
+
+ Vertx,
+ vertx
+}
+
+
+"Runs the module `herd.examples.schedule.chime`."
+shared void run() {
+ value v = vertx.vertx();
+ v.deployVerticle (
+ "ceylon:herd.schedule.chime/0.1.0",
+ ( String|Throwable res ) {
+ if ( is String res ) {
+ value scheduler = Scheduler( v );
+ scheduler.initialize();
+ }
+ else {
+ print( "deploying error ``res``");
+ }
+ }
+ );
+}
+
+
+"Performs scheduler run. Creates cron-style timer and listens it.
+ "
+class Scheduler( Vertx v, String address = "chime" )
+{
+ EventBus eventBus = v.eventBus();
+
+
+ "Initializes testing - creates schedule manager and timer."
+ shared void initialize() {
+ eventBus.send (
+ address,
+ JSON {
+ "operation" -> "create",
+ "name" -> "schedule manager",
+ "state" -> "running"
+ },
+ ( Throwable | Message msg ) {
+ if ( is Message msg ) {
+ managerCreated( msg );
+ }
+ else {
+ print( "error in onConnect ``msg``" );
+ v.close();
+ }
+ }
+ );
+ }
+
+
+ void printMessage( Throwable | Message msg ) {
+ if ( is Message msg ) {
+ if ( exists body = msg.body() ) {
+ print( body );
+ if ( is String state = body.get( "state" ), state == "completed" ) {
+ v.close();
+ }
+ }
+ else {
+ print( "no body in the message" );
+ v.close();
+ }
+ }
+ else {
+ print( "error: ``msg``" );
+ }
+ }
+
+
+ void managerCreated( Message msg ) {
+
+ eventBus.consumer( "schedule manager:timer", printMessage );
+
+ eventBus.send(
+ address,
+ JSON {
+ "operation" -> "create",
+ "name" -> "schedule manager:timer",
+ "state" -> "running",
+ "publish" -> false,
+ "max count" -> 3,
+ "time zone" -> "Europe/Paris",
+ "descirption" -> JSON {
+ "type" -> "cron",
+ "seconds" -> "27/30",
+ "minutes" -> "*",
+ "hours" -> "0-23",
+ "days of month" -> "1-31",
+ "months" -> "*",
+ "days of week" -> "*",
+ "years" -> "2015-2019"
+ }
+ },
+ printMessage
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/source/herd/schedule/chime/ChimeScheduler.ceylon b/source/herd/schedule/chime/ChimeScheduler.ceylon
new file mode 100644
index 0000000..dc3d8d0
--- /dev/null
+++ b/source/herd/schedule/chime/ChimeScheduler.ceylon
@@ -0,0 +1,70 @@
+import io.vertx.ceylon.core {
+
+ Verticle
+}
+import ceylon.json {
+
+ JSON = Object
+}
+import herd.schedule.chime.timer {
+
+ definitions,
+ TimerFactory,
+ StandardTimerFactory
+}
+
+
+"Chime scheduler verticle. Starts scheduling."
+by( "Lis" )
+shared class ChimeScheduler() extends Verticle()
+{
+
+ "Scheduler manager."
+ variable SchedulerManager? scheduler = null;
+
+ "Address to listen on event buss."
+ variable String address = definitions.defaultAddress;
+
+ "Max year limitation."
+ variable Integer maxYearPeriod = 10;
+
+ "Tolerance to compare fire time and current time in milliseconds."
+ variable Integer tolerance = 10;
+
+ "Factory to create timers - refine to produce timers of nonstandard types.
+ Standard factory creates cron-like timer and interval timer."
+ TimerFactory timerFactory = StandardTimerFactory( maxYearPeriod ).initialize();
+
+
+ "Reads configuration from json."
+ void readConfiguration( "Configuration in JSON format." JSON config ) {
+ // read listening address
+ if ( is String addr = config.get( definitions.address ) ) {
+ address = addr;
+ }
+ // year period limitation
+ if ( is Integer maxYear = config.get ( definitions.maxYearPeriodLimit ) ) {
+ if ( maxYear < 100 ) { maxYearPeriod = maxYear; }
+ else { maxYearPeriod = 100; }
+ }
+ // tolerance to compare times
+ if ( is Integer tol = config.get ( definitions.tolerance ) ) {
+ if ( tol > 0 ) { tolerance = tol; }
+ }
+ }
+
+
+ "Starts _Chime_. Called by Vert.x during deployement."
+ shared actual void start() {
+ // read configuration
+ if ( exists c = config ) {
+ readConfiguration( c );
+ }
+
+ // create scheduler
+ SchedulerManager sch = SchedulerManager( vertx, vertx.eventBus(), timerFactory, tolerance );
+ scheduler = sch;
+ sch.connect( address );
+ }
+
+}
diff --git a/source/herd/schedule/chime/Operator.ceylon b/source/herd/schedule/chime/Operator.ceylon
new file mode 100644
index 0000000..e9c5d76
--- /dev/null
+++ b/source/herd/schedule/chime/Operator.ceylon
@@ -0,0 +1,98 @@
+import ceylon.json {
+
+ JSON=Object
+}
+import io.vertx.ceylon.core.eventbus {
+
+ Message,
+ EventBus
+}
+import herd.schedule.chime.timer {
+ definitions
+}
+
+
+"Provides basic operations with [[JSON]] message."
+see( `class SchedulerManager`, `class TimeScheduler` )
+by( "Lis" )
+abstract class Operator( "EventBus to pass messages." shared EventBus eventBus )
+{
+
+ "Operators map."
+ variable Map)>? operators = null;
+
+ "creates operators map."
+ shared formal Map)> createOperators();
+
+
+ "Returns operator by operation code."
+ shared Anything(Message)? getOperator( "operation code" String code ) {
+ if ( !operators exists ) {
+ // create operators map if doesn't exists
+ operators = createOperators();
+ }
+ return operators?.get( code );
+ }
+
+ "Responds on message.
+ respond format:
+ {
+ response -> String // response code
+ error -> String // error description
+ name -> String // item name
+ state -> String // item state
+ description -> JSON // item description
+ }
+ "
+ shared void respondMessage( "Message to respond on." Message msg, "Rreply to be send" JSON reply ) {
+ reply.put( definitions.fieldResponse, definitions.responseOK );
+ msg.reply( reply );
+ }
+
+ "Fails message with message."
+ shared void failMessage (
+ "Message to be responded with failure." Message msg,
+ "Error to fail with." String errorMessage )
+ {
+ msg.reply (
+ JSON {
+ definitions.fieldResponse -> definitions.responseError,
+ definitions.fieldError -> errorMessage
+ }
+ );
+ }
+
+ "Extracts state from request, helper method."
+ shared TimerState? extractState( JSON request ) {
+ if ( is String state = request.get( definitions.fieldState ) ) {
+ return timerRunning.byName( state );
+ }
+ else {
+ return null;
+ }
+ }
+
+ "Message has been received from event bus - process it!."
+ void onMessage( "Message from event bus." Message msg ) {
+ if ( exists request = msg.body(), is String operation = request.get( definitions.fieldOperation ) ) {
+ // depending on operation code
+ if ( exists operator = getOperator( operation ) ) {
+ operator( msg );
+ }
+ else {
+ failMessage( msg, errorMessages.unsupportedOperation );
+ }
+ }
+ else {
+ // response with wrong format error
+ failMessage( msg, errorMessages.operationIsNotSpecified );
+ }
+ }
+
+ "Connects to event bus, returns promise resolved when event listener registered."
+ shared default void connect( "Address to listen to." String address ) {
+ // setup event bus listener
+ eventBus.consumer( address, onMessage );
+ }
+
+}
diff --git a/source/herd/schedule/chime/SchedulerManager.ceylon b/source/herd/schedule/chime/SchedulerManager.ceylon
new file mode 100644
index 0000000..5d9e887
--- /dev/null
+++ b/source/herd/schedule/chime/SchedulerManager.ceylon
@@ -0,0 +1,270 @@
+import io.vertx.ceylon.core {
+
+ Vertx
+}
+import io.vertx.ceylon.core.eventbus {
+
+ Message,
+ EventBus
+}
+import ceylon.json {
+
+ JSON=Object,
+ JSONArray=Array
+}
+import ceylon.collection {
+
+ HashMap
+}
+import herd.schedule.chime.timer {
+ TimerFactory,
+ definitions
+}
+
+
+"manages shcedulers - [[TimeScheduler]]:
+ * creates
+ * deletes
+ * starts
+ * pauses
+ * schedulers info
+
+ Instances of the class are used internaly by Chime.
+ All operations performed as response on request send to general Chime address, \"chime\" by default.
+ Or specified in configuration file.
+ Scheduler to be created before any operations with timers requested.
+
+ ### Requesting:
+
+ expects messages in `JSON` format:
+ {
+ \"operation\" -> String // operation code, mandatory
+ \"name\" -> String // scheduler or full timer (\"scheduler name:timer name\") name, mandatory
+ \"state\" -> String // state, mandatory only for state operation
+ }
+
+ If timer name specified as *\"scheduler name:timer name\"* operation is performed for timer -
+ see description in [[TimeScheduler]] otherwise for scheduler - see description below.
+
+ #### operation codes:
+ * \"create\" - create new scheduler with specified name, state and description, if state is not specified, sceduler to be run.
+ If full timer name specified *`scheduler name`:`timer name`* timer is to be created, if no scheduler with \"scheduler name\"
+ has been created before, it will be created.
+ * \"delete\" - delete scheduler with name `name` (or timer if full timer name specified *\"scheduler name:timer name\"*)
+ * \"info\" - requesting info on Chime, specific scheduler (scheduler name to be provided) or
+ timer (full timer name specified *\"scheduler name:timer name\"* to be provided)
+ * \"state\":
+ * if is \"get\" state is to be returned
+ * if is \"running\" scheduler is to be run if not already
+ * if is \"paused\" scheduler is to be paused if not already
+ * otherwise error is returned
+
+ #### examples:
+ // create new scheduler with name \"scheduler name\"
+ JSON message = JSON {
+ \"operation\" -> \"create\",
+ \"name\" -> \"scheduler name\"
+ }
+
+ // change state of scheduler with \"scheduler name\" on paused
+ JSON message = JSON {
+ \"operation\" -> \"state\",
+ \"name\" -> \"scheduler name\",
+ \"state\" -> \"paused\"
+ }
+
+ ### Response
+ response on messages is in `JSON`:
+ {
+ \"response\" -> String // response code - one of \"ok\" or \"error\"
+ \"name\" -> String // scheduler or full timer (\"scheduler name:timer name\") name
+ \"state\" -> String // scheduler state
+ \"schedulers\" -> JSONArray // scheduler names, exists as response on \"info\" operation with no \"name\" field
+ \"error\" -> String // error description, exists only if response == \"error\"
+ }
+
+ "
+by( "Lis" )
+see(`class TimeScheduler`)
+class SchedulerManager(
+ "Vetrx the scheduler is running on." Vertx vertx,
+ "Event bus used to dispatch messages." EventBus eventBus,
+ "Factory to create timers" TimerFactory factory,
+ "Tolerance to compare fire time and current time in miliseconds." Integer tolerance
+)
+ extends Operator( eventBus )
+{
+
+ "Time schedulers."
+ HashMap schedulers = HashMap();
+
+ TimerCreator creator = TimerCreator( factory );
+
+ "Returns scheduler by its name or `null` if doesn't exist."
+ shared TimeScheduler? getScheduler( "Name of the scheduler looked for." String name ) => schedulers.get( name );
+
+ "Adds new scheduler.
+ Retruns new or already existed shceduler with name `name`."
+ shared TimeScheduler addScheduler( "Scheduler name." String name, "Scheduler state." TimerState state ) {
+ if ( exists sch = getScheduler( name ) ) {
+ return sch;
+ }
+ else {
+ TimeScheduler sch = TimeScheduler( name, vertx, eventBus, creator, tolerance );
+ schedulers.put( name, sch );
+ sch.connect( name );
+ if ( state == timerRunning ) {
+ sch.start();
+ }
+ return sch;
+ }
+ }
+
+
+// operation methods
+
+ "Creates operators map"
+ shared actual Map)> createOperators()
+ => map)> {
+ definitions.opCreate -> operationCreate,
+ definitions.opDelete -> operationDelete,
+ definitions.opState -> operationState,
+ definitions.opInfo -> operationInfo
+ };
+
+ "Processes 'create new scheduler' operation."
+ void operationCreate( Message msg ) {
+ if ( exists request = msg.body(), is String name = request.get( definitions.fieldName ) ) {
+
+ String schedulerNamer;
+ String timerName;
+ if ( exists inc = name.firstInclusion( definitions.nameSeparator ) ) {
+ schedulerNamer = name.spanTo( inc - 1 );
+ timerName = name;
+ }
+ else {
+ schedulerNamer = name;
+ timerName = "";
+ }
+ value scheduler = addScheduler( schedulerNamer, extractState( request ) else timerRunning );
+ if ( timerName.empty ) {
+ failMessage( msg, errorMessages.timerAlreadyExists );
+ }
+ else {
+ // add timer to scheduler
+ scheduler.operationCreate( msg );
+ }
+ }
+ else {
+ // response with wrong format error
+ failMessage( msg, errorMessages.schedulerNameHasToBeSpecified );
+ }
+ }
+
+ "Processes 'delete scheduler' operation."
+ void operationDelete( Message msg ) {
+ if ( exists request = msg.body(), is String name = request.get( definitions.fieldName ) ) {
+ // delete scheduler
+ if ( exists sch = schedulers.remove( name ) ) {
+ sch.stop();
+ // scheduler successfully removed
+ respondMessage( msg, sch.shortInfo );
+ }
+ else {
+ // scheduler doesn't exists - look if name is full timer name
+ value schedulerName = name.spanTo( ( name.firstInclusion( definitions.nameSeparator ) else 0 ) - 1 );
+ if ( !schedulerName.empty, exists sch = schedulers.get( schedulerName ) ) {
+ // scheduler has to remove timer
+ sch.operationDelete( msg );
+ }
+ else {
+ // scheduler or timer doesn't exist
+ failMessage( msg, errorMessages.schedulerNotExists );
+ }
+ }
+ }
+ else {
+ // response with wrong format error
+ failMessage( msg, errorMessages.schedulerNameHasToBeSpecified );
+ }
+ }
+
+ "Processes 'scheduler state' operation."
+ void operationState( Message msg ) {
+ if ( exists request = msg.body(), is String name = request.get( definitions.fieldName ) ) {
+ if ( is String state = request.get( definitions.fieldState ) ) {
+ if ( exists sch = schedulers.get( name ) ) {
+ if ( state == definitions.stateGet ) {
+ // return state
+ respondMessage( msg, sch.shortInfo );
+ }
+ else if ( state == timerPaused.string ){
+ // set paused state
+ sch.pause();
+ respondMessage( msg, sch.shortInfo );
+ }
+ else if ( state == timerRunning.string ){
+ // set running state
+ sch.start();
+ respondMessage( msg, sch.shortInfo );
+ }
+ else {
+ // state to be one of - get, paused, running
+ failMessage( msg, errorMessages.incorrectTimerState );
+ }
+ }
+ else {
+ // scheduler doesn't exists - look if name is full timer name
+ value schedulerName = name.spanTo( ( name.firstInclusion( definitions.nameSeparator ) else 0 ) - 1 );
+ if ( !schedulerName.empty, exists sch = schedulers.get( schedulerName ) ) {
+ // scheduler has to provide timer state
+ sch.operationState( msg );
+ }
+ else {
+ // scheduler or timer doesn't exist
+ failMessage( msg, errorMessages.schedulerNotExists );
+ }
+ }
+ }
+ else {
+ // scheduler state to be specified
+ failMessage( msg, errorMessages.stateToBeSpecified );
+ }
+ }
+ else {
+ // scheduler name to be specified
+ failMessage( msg, errorMessages.schedulerNameHasToBeSpecified);
+ }
+ }
+
+ "Replies with Chime info - array of scheduler names."
+ void operationInfo( Message msg ) {
+ if ( is String name = msg.body()?.get( definitions.fieldName ) ) {
+ if ( exists sch = schedulers.get( name ) ) {
+ // reply with scheduler info
+ msg.reply( sch.fullInfo );
+ }
+ else {
+ // scheduler doesn't exists - look if name is full timer name
+ value schedulerName = name.spanTo( ( name.firstInclusion( definitions.nameSeparator ) else 0 ) - 1 );
+ if ( !schedulerName.empty, exists sch = schedulers.get( schedulerName ) ) {
+ // scheduler has to reply for timer info
+ sch.operationInfo( msg );
+ }
+ else {
+ // scheduler or timer doesn't exist
+ failMessage( msg, errorMessages.schedulerNotExists );
+ }
+ }
+ }
+ else {
+ msg.reply (
+ JSON {
+ definitions.fieldResponse -> definitions.responseOK,
+ definitions.fieldSchedulers -> JSONArray( { for ( scheduler in schedulers.items ) scheduler.name } )
+ }
+ );
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/source/herd/schedule/chime/TimeConverter.ceylon b/source/herd/schedule/chime/TimeConverter.ceylon
new file mode 100644
index 0000000..668b4ae
--- /dev/null
+++ b/source/herd/schedule/chime/TimeConverter.ceylon
@@ -0,0 +1,90 @@
+import ceylon.time {
+
+ DateTime,
+ Instant
+}
+import java.util {
+
+ JavaTimeZone=TimeZone
+}
+import ceylon.json {
+
+ JSON=Object
+}
+import herd.schedule.chime.timer {
+
+ definitions
+}
+
+
+"Converting date-time according to rule (timezone)."
+by( "Lis" )
+interface TimeConverter {
+
+ "Converts remote date-time to local one."
+ shared formal DateTime toLocal( "Date-time to convert from." DateTime remote );
+
+ "Converts local date-time to remote one."
+ shared formal DateTime toRemote( "Date-time to convert from." DateTime local );
+
+ "Returns time zone id."
+ shared formal String timeZoneID;
+}
+
+
+"Defines time converter which do no convertion."
+by( "Lis" )
+object dummyConverter satisfies TimeConverter {
+
+ "Local time zone."
+ shared JavaTimeZone local = JavaTimeZone.default;
+
+ "Returns converter by time zone name."
+ shared TimeConverter? getConverter( JSON description ) {
+ if ( is String timeZoneID = description.get( definitions.timeZoneID ) ) {
+ JavaTimeZone tz = JavaTimeZone.getTimeZone( timeZoneID );
+ if ( tz.id == timeZoneID ) {
+ return ConverterWithTimezone( tz );
+ }
+ else {
+ return null;
+ }
+ }
+ else {
+ return dummyConverter;
+ }
+ }
+
+ "Returns `remote`."
+ shared actual DateTime toLocal( DateTime remote ) => remote;
+
+ "Returns `local`."
+ shared actual DateTime toRemote( DateTime local ) => local;
+
+ shared actual String timeZoneID => local.id;
+
+}
+
+
+"Converts according to specified remote time zone."
+by( "Lis" )
+class ConverterWithTimezone( JavaTimeZone remoteTimeZone ) satisfies TimeConverter {
+
+
+ shared actual DateTime toLocal( DateTime remote ) {
+ Integer remoteTime = remote.instant().millisecondsOfEpoch;
+ Integer utcTime = remoteTime - remoteTimeZone.getOffset( remoteTime );
+ Integer localUTCOffset = dummyConverter.local.getOffset( utcTime + dummyConverter.local.getOffset( utcTime ) );
+ return Instant( utcTime + localUTCOffset ).dateTime();
+ }
+
+ shared actual DateTime toRemote( DateTime local ) {
+ Integer localTime = local.instant().millisecondsOfEpoch;
+ Integer utcTime = localTime - dummyConverter.local.getOffset( localTime );
+ Integer remoteUTCOffset = remoteTimeZone.getOffset( utcTime + remoteTimeZone.getOffset( utcTime ) );
+ return Instant( utcTime + remoteUTCOffset ).dateTime();
+ }
+
+ shared actual String timeZoneID => remoteTimeZone.id;
+
+}
diff --git a/source/herd/schedule/chime/TimeScheduler.ceylon b/source/herd/schedule/chime/TimeScheduler.ceylon
new file mode 100644
index 0000000..f40c01f
--- /dev/null
+++ b/source/herd/schedule/chime/TimeScheduler.ceylon
@@ -0,0 +1,521 @@
+import io.vertx.ceylon.core {
+
+ Vertx
+}
+import io.vertx.ceylon.core.eventbus {
+
+ Message,
+ EventBus
+}
+import ceylon.json {
+
+ JSON=Object,
+ JSONArray=Array
+}
+import ceylon.collection {
+
+ HashMap
+}
+import ceylon.time {
+
+ systemTime,
+ Period,
+ zero,
+ DateTime
+}
+import ceylon.time.timezone {
+
+ timeZone
+}
+import herd.schedule.chime.timer {
+ definitions
+}
+import herd.schedule.chime.cron {
+
+ calendar
+}
+
+
+"Scheduler - listen address of scheduler [[name]] on event bus and manages timers.
+ This class is used internaly by Chime
+
+ ### Requests
+
+ Requests are send in [[JSON]] format on scheduler name address
+ {
+ \"operation\" -> String // operation code, mandatory
+ \"name\" -> String // timer short or full name, mandatory
+ \"state\" -> String // state, nonmandatory, except sate operation
+
+ // fields for create operation:
+ \"maximum count\" -> Integer // maximum number of fires, default - unlimited
+ \"publish\" -> Boolean // if true message to be published and send otherwise, nonmandatory
+
+ \"start time\" -> `JSON` // start time, nonmadatory, if doesn't exists timer will start immediately
+ {
+ \"seconds\" -> Integer // seconds, mandatory
+ \"minutes\" -> Integer // minutes, mandatory
+ \"hours\" -> Integer // hours, mandatory
+ \"day of month\" -> Integer // days of month, mandatory
+ \"month\" -> Integer or String // months, if string - short name, mandatory
+ \"year\" -> Integer // year, Integer, mandatory
+ }
+
+ \"end time\" -> `JSON` // end time, nonmadatory, default no end time
+ {
+ \"seconds\" -> Integer // seconds, mandatory
+ \"minutes\" -> Integer // minutes, mandatory
+ \"hours\" -> Integer // hours, mandatory
+ \"day of month\" -> Integer // days of month, mandatory
+ \"month\" -> Integer or String // months, if string - short name, mandatory
+ \"year\" -> Integer // year, Integer, mandatory
+ }
+
+ \"time zone\" -> String // time zone name, nonmandatory, default server local
+
+ \"description\" -> JSON // timer desciption, mandatoty for create operation
+ }
+
+ timer full name is *'scheduler name':'timer short name'*
+
+ #### operation codes:
+ * \"create\" - create new timer with specified name, state and description
+ * \"delete\" - delete timer with name `name`
+ * \"info\" - get information on timer (if timer name is specified) or scheduler (if timer name is not specified)
+ * \"state\":
+ * if state field is \"get\" state is to be returned
+ * if state field is \"running\" timer is to be run if not already
+ * if state field is \"paused\" timer is to be paused if not already
+ * otherwise error is returned
+
+ #### supported timers (types):
+ * cron style, defined like cron, but with some simplifications
+ * incremental, fires after each specified period (minimum 1 second)
+
+ #### timer description in depends on the timer type:
+
+ * cron style timer description:
+ {
+ \"type\" -> String // timer type, mandatory
+
+ \"seconds\" -> String // seconds in cron style, mandatory
+ \"minutes\" -> String // minutes in cron style, mandatory
+ \"hours\" -> String // hours in cron style, mandatory
+ \"day of month\" -> String // days of month in cron style, mandatory
+ \"month\" -> String // months in cron style, mandatory
+ \"day of week\" -> String // days of week in cron style, L means last, # means nth of month, nonmandatory
+ \"year\" -> String // year in cron style, nonmandatory
+ }
+
+ * interval timer description:
+ {
+ \"type\" -> String // timer type, mandatory
+ \"increment seconds\" -> Integer // increment in seconds, if <= 0 timer fires only once, mandatory
+ }
+
+
+ ### Response.
+
+ Scheduler responses on each request in [[JSON]] format:
+ {
+ \"response\" -> String // response code - one of `ok` or `error`
+ \"name\" -> String // timer name
+ \"state\" -> String // state
+
+ \"error\" -> String // error description, exists only if response == `error`
+ \"timers\" -> JSONArray // list of timer names currently scheduled - response on info operation with no name field specified
+
+ // Info operation returns fields from create operation also
+ }
+
+ "
+by( "Lis" )
+class TimeScheduler(
+ "Scheduler name." shared String name,
+ "Vertx the scheduler operates on." Vertx vertx,
+ "EventBus to pass messages." EventBus eventBus,
+ "Factory to create timers." TimerCreator factory,
+ "Tolerance to compare fire time and current time in miliseconds." Integer tolerance
+ )
+ extends Operator( eventBus )
+{
+
+ "Tolerance to compare fire time."
+ variable Period tolerancePeriod = zero.plusMilliseconds( tolerance );
+
+ "Timers sorted by next fire time."
+ HashMap timers = HashMap();
+
+ "Id of vertx timer."
+ variable Integer? timerID = null;
+
+ "Value for scheduler state - running, paused or completed."
+ variable TimerState schedulerState = timerPaused;
+
+ "Scheduler state - running, paused or completed."
+ shared TimerState state => schedulerState;
+
+ "Scheduler `JSON` short info (name and state)."
+ shared JSON shortInfo
+ => JSON {
+ definitions.fieldName -> name,
+ definitions.fieldState -> schedulerState.string
+ };
+
+ "Scheduler full info in `JSON`:
+ \"response\" -> ok,
+ \"name\" -> scheduler name,
+ \"state\" -> scheduler state
+ \"timers\" -> array of timer names
+ "
+ shared JSON fullInfo => JSON {
+ definitions.fieldResponse -> definitions.responseOK,
+ definitions.fieldName -> name,
+ definitions.fieldState -> state.string,
+ definitions.fieldTimers -> JSONArray( { for ( timer in timers.items ) timer.name } )
+ };
+
+
+// timers map methods
+
+ "`true` if timer is running."
+ Boolean selectRunning( TimerContainer timer ) => timer.state == timerRunning;
+
+ "`true` if timer is completed."
+ Boolean selectCompleted( TimerContainer timer ) => timer.state == timerCompleted;
+
+ "`true` if timer is running or paused."
+ Boolean selectIncompleted( TimerContainer timer ) => timer.state != timerCompleted;
+
+ "Name of the given timer."
+ String timerName( TimerContainer timer ) => timer.name;
+
+ "Remove completed timers."
+ void removeCompleted() => timers.removeAll( timers.items.filter( selectCompleted ).map( timerName ) );
+
+
+ "Minimum timer delay."
+ Integer minDelay() {
+ DateTime current = localTime();
+ variable Integer delay = 0;
+ for ( timer in timers.items.filter( selectRunning ) ) {
+ if ( exists localDate = timer.localFireTime ) {
+ Integer offset = localDate.offset( current );
+ if ( offset <= 0 ) {
+ if ( delay > 500 || delay == 0 ) {
+ delay = 500;
+ }
+ }
+ else if ( offset < delay || delay == 0 ) {
+ delay = offset;
+ }
+ }
+ }
+ return delay;
+ }
+
+ "Current local time."
+ DateTime localTime() => systemTime.instant().dateTime( timeZone.system );
+
+ "Fire timers, returns `true` if some timer has been fired and `false` if no one timer has been fired."
+ void fireTimers() {
+ variable Boolean completed = false;
+ DateTime current = localTime().plus( tolerancePeriod );
+ for ( timer in timers.items.filter( selectRunning ) ) {
+ if ( exists localDate = timer.localFireTime, exists remoteDate = timer.remoteFireTime ) {
+ if ( localDate < current ) {
+ timer.shiftTime();
+ sendTimerMessage( timer, remoteDate );
+ if ( timer.state == timerCompleted ) {
+ sendTimerMessage( timer );
+ completed = true;
+ }
+ }
+ }
+ }
+ if ( completed ) {
+ removeCompleted();
+ }
+ }
+
+ "Sends fire or completed message in standard Chime format.
+
+ message format:
+ {
+ \"name\": timer name, String
+ \"time\": String formated time / date from [[TimerContainer.remoteFireTime]] or nothing if not specified
+ \"count\": total number of fire times
+ \"state\": String representation of [[TimerContainer.state]]
+ }
+ "
+ shared void sendTimerMessage( TimerContainer timer, DateTime? date = null ) {
+ JSON message;
+
+ // date string
+ if ( exists date ) {
+ message = JSON {
+ definitions.fieldName -> timer.name,
+ definitions.fieldTime -> date.string,
+ definitions.fieldCount -> timer.count,
+ definitions.fieldState -> timer.state.string,
+ calendar.seconds -> date.seconds,
+ calendar.minutes -> date.minutes,
+ calendar.hours -> date.hours,
+ calendar.dayOfMonth -> date.day,
+ calendar.month -> date.month.integer,
+ calendar.year -> date.year,
+ definitions.timeZoneID -> timer.timeZoneID
+ };
+ }
+ else {
+ message = JSON {
+ definitions.fieldName -> timer.name,
+ definitions.fieldCount -> timer.count,
+ definitions.fieldState -> timer.state.string
+ };
+ }
+
+ // send message
+ if ( timer.publish ) {
+ eventBus.publish( timer.name, message );
+ }
+ else {
+ eventBus.send( timer.name, message );
+ }
+ }
+
+
+// vertx timer
+
+ "Cancels current vertx timer."
+ void cancelCurrentVertxTimer() {
+ if ( exists id = timerID ) {
+ timerID = null;
+ vertx.cancelTimer( id );
+ }
+ }
+
+ "Builds vertx timer using min delay."
+ void buildVertxTimer() {
+ cancelCurrentVertxTimer();
+ Integer delay = minDelay();
+ if ( delay > 0 ) {
+ timerID = vertx.setTimer( delay, vertxTimerFired );
+ }
+ }
+
+ "Vertx timer has been fired - send message and use next vertx timer."
+ void vertxTimerFired( Integer id ) {
+ if ( state == timerRunning, exists currentID = timerID, currentID == id ) {
+ timerID = null;
+ fireTimers();
+ buildVertxTimer();
+ }
+ }
+
+
+// operations
+
+ "Returns timer full name from short name."
+ String timerFullName( String timerShortName ) {
+ if ( timerShortName.startsWith( name + definitions.nameSeparator ) ) {
+ return timerShortName;
+ }
+ else {
+ return name + definitions.nameSeparator + timerShortName;
+ }
+ }
+
+ "Adds timer to timers - to be added according to next fire time sort.
+ If timer has been added previously it will be replaced."
+ void addTimer (
+ "timer to be added" TimerContainer timer,
+ "timer state" TimerState state )
+ {
+ if ( state == timerRunning ) {
+ timer.start( localTime() );
+ if ( timer.state == timerRunning ) {
+ timers.put( timer.name, timer );
+ buildVertxTimer();
+ }
+ else {
+ sendTimerMessage( timer );
+ }
+ }
+ else if ( state == timerPaused ) {
+ timers.put( timer.name, timer );
+ }
+ }
+
+
+ "Creates operators map."
+ shared actual Map)> createOperators()
+ => map)> {
+ definitions.opCreate -> operationCreate,
+ definitions.opDelete -> operationDelete,
+ definitions.opState -> operationState,
+ definitions.opInfo -> operationInfo
+ };
+
+ "Creates new timer."
+ shared void operationCreate( Message msg ) {
+ if ( exists request = msg.body(), is String tName = request.get( definitions.fieldName ) ) {
+ String timerName = timerFullName( tName );
+ if ( timers.defines( timerName ) ) {
+ // timer already exists
+ failMessage( msg, errorMessages.timerAlreadyExists );
+ }
+ else {
+ value timer = factory.createTimer( timerFullName( tName ), request );
+ if ( is TimerContainer timer ) {
+ addTimer( timer, extractState( request ) else timerRunning );
+ // timer successfully added
+ respondMessage( msg, timer.stateDescription() );
+ }
+ else {
+ // wrong description
+ failMessage( msg, timer );
+ }
+ }
+ }
+ else {
+ // timer name to be specified
+ failMessage( msg, errorMessages.timerNameHasToBeSpecified );
+ }
+
+ }
+
+ "Deletes existing timer."
+ shared void operationDelete( Message msg ) {
+ if ( exists request = msg.body(), is String tName = request.get( definitions.fieldName ) ) {
+ String timerName = timerFullName( tName );
+ // delete timer
+ if ( exists t = timers.remove( timerName ) ) {
+ t.complete();
+ // timer successfully removed
+ respondMessage( msg, t.stateDescription() );
+ }
+ else {
+ // timer doesn't exist
+ failMessage( msg, errorMessages.timerNotExists );
+ }
+ }
+ else {
+ // timer name to be specified
+ failMessage( msg, errorMessages.timerNameHasToBeSpecified );
+ }
+ }
+
+ "Processes 'timer state' operation."
+ shared void operationState( Message msg ) {
+ if ( exists request = msg.body(), is String tName = request.get( definitions.fieldName ) ) {
+ if ( is String state = request.get( definitions.fieldState ) ) {
+ String timerName = timerFullName( tName );
+ if ( exists t = timers.get( timerName ) ) {
+ if ( state == definitions.stateGet ) {
+ // return state
+ respondMessage( msg, t.stateDescription() );
+ }
+ else if ( state == timerPaused.string ){
+ // set paused state
+ t.state = timerPaused;
+ respondMessage( msg, t.stateDescription() );
+ }
+ else if ( state == timerRunning.string ) {
+ // set running state
+ if ( t.state == timerPaused ) {
+ t.start( localTime() );
+ if ( t.state == timerRunning ) {
+ buildVertxTimer();
+ }
+ else {
+ timers.remove( t.name );
+ sendTimerMessage( t );
+ }
+ }
+ respondMessage( msg, t.stateDescription() );
+ }
+ else {
+ // state to be one of - get, paused, running
+ failMessage( msg, errorMessages.incorrectTimerState );
+ }
+ }
+ else {
+ // timer doesn't exist
+ failMessage( msg, errorMessages.timerNotExists );
+ }
+ }
+ else {
+ // timer state to be specified
+ failMessage( msg, errorMessages.stateToBeSpecified );
+ }
+ }
+ else {
+ // timer name to be specified
+ failMessage( msg, "timer name has to be specified" );
+ }
+ }
+
+ "Replies with scheduler info - array of timer names."
+ shared void operationInfo( Message msg ) {
+ if ( exists request = msg.body(), is String tName = request.get( definitions.fieldName ) ) {
+ // contains name field - reply with info about timer with specified name
+ String timerName = timerFullName( tName );
+ if ( exists t = timers.get( timerName ) ) {
+ // timer successfully removed
+ respondMessage( msg, t.fullDescription() );
+ }
+ else {
+ // timer doesn't exist
+ failMessage( msg, errorMessages.timerNotExists );
+ }
+ }
+ else {
+ // timer name to be specified
+ failMessage( msg, errorMessages.timerNameHasToBeSpecified );
+ }
+ }
+
+
+// scheduler methods
+
+ "Starts scheduling."
+ see( `function pause` )
+ see( `function stop` )
+ shared void start() {
+ if ( state != timerRunning ) {
+ schedulerState = timerRunning;
+ DateTime current = localTime();
+ for ( timer in timers.items.filter( selectRunning ) ) {
+ timer.start( current );
+ if ( timer.state == timerCompleted ) {
+ sendTimerMessage( timer );
+ }
+ }
+ removeCompleted();
+ buildVertxTimer();
+ }
+ }
+
+ "Pauses scheduling - all fires to be missed while start not called."
+ see( `function start` )
+ shared void pause() {
+ schedulerState = timerPaused;
+ cancelCurrentVertxTimer();
+ }
+
+ "Completes all timers and terminates this scheduler."
+ shared void stop() {
+ schedulerState = timerCompleted;
+
+ // fire completed on all timers
+ for ( timer in timers.items.filter( selectIncompleted ) ) {
+ timer.complete();
+ sendTimerMessage( timer );
+ }
+ timers.clear();
+
+ cancelCurrentVertxTimer();
+ }
+
+}
diff --git a/source/herd/schedule/chime/TimerContainer.ceylon b/source/herd/schedule/chime/TimerContainer.ceylon
new file mode 100644
index 0000000..3054b28
--- /dev/null
+++ b/source/herd/schedule/chime/TimerContainer.ceylon
@@ -0,0 +1,179 @@
+import ceylon.json {
+
+ JSON=Object
+}
+import ceylon.time {
+
+ DateTime
+}
+import herd.schedule.chime.timer {
+ definitions,
+ Timer
+}
+import herd.schedule.chime.cron {
+
+ calendar
+}
+
+
+"Posses timer."
+by( "Lis" )
+class TimerContainer (
+ "Timer full name, which is *'scheduler name':'timer name'*." shared String name,
+ "Timer [[JSON]] descirption" shared JSON description,
+ "`true` if message to be published and `false` if message to be send" shared Boolean publish,
+ "Timer within this container." Timer timer,
+ "Remote-local date-time converter." TimeConverter converter,
+ "Max count or null if not specified." shared Integer? maxCount,
+ "Timer start time or null if to be started immediately." shared DateTime? startTime,
+ "Timer end time or null if not specified." shared DateTime? endTime
+) {
+
+ "Timer fire counting."
+ shared variable Integer count = 0;
+
+ "Timer state."
+ shared variable TimerState state = timerPaused;
+
+ "Next fire timer in remote TZ or null if completed."
+ variable DateTime? nextRemoteFireTime = null;
+
+ "Next fire timer in remote TZ or null if completed."
+ shared DateTime? remoteFireTime => nextRemoteFireTime;
+
+ "Next fire timer in remote TZ or null if completed."
+ shared DateTime? localFireTime => if ( exists d = nextRemoteFireTime ) then converter.toLocal( d ) else null;
+
+ "Time zone ID."
+ shared String timeZoneID => converter.timeZoneID;
+
+ "Timer name + state."
+ shared JSON stateDescription() {
+ return JSON {
+ definitions.fieldName -> name,
+ definitions.fieldState -> state.string
+ };
+ }
+
+ "Returns _full_ timer description."
+ shared JSON fullDescription() {
+ JSON descr = JSON {
+ definitions.fieldName -> name,
+ definitions.fieldState -> state.string,
+ definitions.fieldCount -> count,
+ definitions.fieldDescription -> description,
+ definitions.fieldPublish -> publish
+ };
+
+ if ( exists d = maxCount ) {
+ descr.put( definitions.fieldMaxCount, d );
+ }
+
+ if ( exists d = startTime ) {
+ descr.put (
+ definitions.fieldStartTime,
+ JSON {
+ calendar.seconds -> d.seconds,
+ calendar.minutes -> d.minutes,
+ calendar.hours -> d.hours,
+ calendar.dayOfMonth -> d.day,
+ calendar.month -> d.month.string,
+ calendar.year -> d.year
+ }
+ );
+ }
+
+ if ( exists d = endTime ) {
+ descr.put (
+ definitions.fieldEndTime,
+ JSON {
+ calendar.seconds -> d.seconds,
+ calendar.minutes -> d.minutes,
+ calendar.hours -> d.hours,
+ calendar.dayOfMonth -> d.day,
+ calendar.month -> d.month.string,
+ calendar.year -> d.year
+ }
+ );
+ }
+
+ return descr;
+ }
+
+ "Starts the timer."
+ shared void start( DateTime currentLocal ) {
+
+ DateTime currentRemote = converter.toRemote( currentLocal );
+
+ // check if max count has been reached before
+ if ( exists c = maxCount ) {
+ if ( count >= c ) {
+ complete();
+ return;
+ }
+ }
+
+ // check if start time is after current
+ DateTime beginning;
+ if ( exists st = startTime ) {
+ if ( st > currentRemote ) {
+ beginning = st;
+ }
+ else {
+ beginning = currentRemote;
+ }
+ }
+ else {
+ beginning = currentRemote;
+ }
+
+ // start timer
+ if ( exists date = timer.start( beginning ) ) {
+ if ( exists ed = endTime ) {
+ if ( date > ed ) {
+ complete();
+ return;
+ }
+ }
+ state = timerRunning;
+ nextRemoteFireTime = date;
+ }
+ else {
+ complete();
+ }
+ }
+
+ "Sets timer completed."
+ shared void complete() {
+ nextRemoteFireTime = null;
+ state = timerCompleted;
+ }
+
+ "Shifts timer to the next time."
+ shared void shiftTime() {
+ if ( state == timerRunning ) {
+ count ++;
+ if ( exists date = timer.shiftTime() ) {
+
+ // check on complete
+
+ if ( exists ed = endTime ) {
+ if ( date > ed ) {
+ complete();
+ return;
+ }
+ }
+
+ if ( exists c = maxCount ) {
+ if ( count >= c ) {
+ complete();
+ return;
+ }
+ }
+
+ nextRemoteFireTime = date;
+ }
+ }
+ }
+
+}
diff --git a/source/herd/schedule/chime/TimerCreator.ceylon b/source/herd/schedule/chime/TimerCreator.ceylon
new file mode 100644
index 0000000..544892e
--- /dev/null
+++ b/source/herd/schedule/chime/TimerCreator.ceylon
@@ -0,0 +1,169 @@
+import ceylon.json {
+
+ JSON=Object
+}
+import ceylon.time {
+
+ dateTime,
+ DateTime
+}
+import herd.schedule.chime.timer {
+
+ TimerFactory,
+ definitions,
+ Timer
+}
+import herd.schedule.chime.cron {
+
+ calendar
+}
+
+
+"Uses [[JSON]] description to creates [[TimerContainer]] with timer [[Timer]] created by timer factory."
+by( "Lis" )
+see( `interface TimerFactory` )
+see( `interface Timer` )
+see( `class TimerContainer` )
+class TimerCreator( "Factory to create timers." TimerFactory factory )
+{
+
+ "Creates timer from creation request."
+ shared TimerContainer|String createTimer( "Timer name." String name, "Tequest with timer description." JSON request ) {
+ if ( is JSON description = request.get( definitions.fieldDescription ) ) {
+ value timer = factory.createTimer( description );
+ if ( is Timer timer ) {
+ return createTimerContainer( request, description, name, timer );
+ }
+ else {
+ return timer;
+ }
+ }
+ else {
+ // timer description to be specified
+ return errorMessages.timerDescriptionHasToBeSpecified;
+ }
+ }
+
+
+ "Creates timer container by container and creation request."
+ TimerContainer|String createTimerContainer (
+ "Request on timer creation." JSON request,
+ "Timer desciption." JSON description,
+ "Timer name." String name,
+ "Timer." Timer timer
+ ) {
+ // extract start date if exists
+ DateTime? startDate;
+ if ( is JSON startTime = request.get( definitions.fieldStartTime ) ) {
+ if ( exists st = extractDate( startTime ) ) {
+ startDate = st;
+ }
+ else {
+ return errorMessages.incorrectStartDate;
+ }
+ }
+ else {
+ startDate = null;
+ }
+
+ // extract end date if exists
+ DateTime? endDate;
+ if ( is JSON endTime = request.get( definitions.fieldEndTime ) ) {
+ if ( exists st = extractDate( endTime ) ) {
+ endDate = st;
+ }
+ else {
+ return errorMessages.incorrectEndDate;
+ }
+ }
+ else {
+ endDate = null;
+ }
+
+ // end date has to be after start!
+ if ( exists st = startDate, exists et = endDate ) {
+ if ( et <= st ) {
+ return errorMessages.endDateToBeAfterStartDate;
+ }
+ }
+
+ if ( exists converter = dummyConverter.getConverter( request ) ) {
+ return TimerContainer (
+ name, description, extractPublish( request ), timer,
+ converter, extractMaxCount( request ), startDate, endDate
+ );
+ }
+ else {
+ return errorMessages.unsupportedTimezone;
+ }
+
+ }
+
+ "Extracts month from field with key key. The field can be either integer or string (like JAN, FEB etc, see [[calendar]])."
+ Integer? extractMonth( JSON description, String key ) {
+ if ( is Integer val = description.get( key ) ) {
+ if ( val > 0 && val < 13 ) {
+ return val;
+ }
+ else {
+ return null;
+ }
+ }
+ else if ( is String val = description.get( key ) ) {
+ if ( exists ret = calendar.monthFullMap.get( val ) ) {
+ return ret;
+ }
+ return calendar.monthShortMap.get( val );
+ }
+ else {
+ return null;
+ }
+ }
+
+ "Extracts date from [[JSON]], key returns [[JSON]] object with date."
+ DateTime? extractDate( JSON date ) {
+ if ( is Integer seconds = date.get( calendar.seconds ),
+ is Integer minutes = date.get( calendar.minutes ),
+ is Integer hours = date.get( calendar.hours ),
+ is Integer dayOfMonth = date.get( calendar.dayOfMonth ),
+ is Integer year = date.get( calendar.year ),
+ exists month = extractMonth( date, calendar.month )
+ ) {
+ try {
+ return dateTime( year, month, dayOfMonth, hours, minutes, seconds );
+ }
+ catch ( Throwable err ) {
+ return null;
+ }
+ }
+ return null;
+ }
+
+ "Extracts publish field from description.
+ `publish` or `send` are nonmandatory field.
+ If no field extracted - default to be send."
+ Boolean extractPublish( JSON description ) {
+ if ( is Boolean b = description.get( definitions.fieldPublish ) ) {
+ return b;
+ }
+ else {
+ return false;
+ }
+ }
+
+ "`maxCount` - nonmandatory field, if not specified - infinitely."
+ Integer? extractMaxCount( JSON description ) {
+ if ( is Integer c = description.get( definitions.fieldMaxCount ) ) {
+ if ( c > 0 ) {
+ return c;
+ }
+ else {
+ return 1;
+ }
+ }
+ else {
+ return null;
+ }
+ }
+
+}
diff --git a/source/herd/schedule/chime/TimerState.ceylon b/source/herd/schedule/chime/TimerState.ceylon
new file mode 100644
index 0000000..2d33f39
--- /dev/null
+++ b/source/herd/schedule/chime/TimerState.ceylon
@@ -0,0 +1,50 @@
+
+
+"Timer state - running, paused or completed."
+by( "Lis" )
+abstract class TimerState()
+ of timerRunning | timerPaused | timerCompleted
+{
+
+ "Returns timer state by name:
+ * \"paused\" - timerPaused
+ * \"running\" - timerRunning
+ * \"completed\" - timerCompleted
+ * otherwise - null
+ "
+ shared TimerState? byName( String name ) {
+ if ( name == timerPaused.string ) {
+ return timerPaused;
+ }
+ else if ( name == timerRunning.string ) {
+ return timerRunning;
+ }
+ else if ( name == timerCompleted.string ) {
+ return timerCompleted;
+ }
+ else {
+ return null;
+ }
+ }
+}
+
+"Timer running state."
+by( "Lis" )
+object timerRunning extends TimerState()
+{
+ shared actual String string = "running";
+}
+
+"Timer paused state."
+by( "Lis" )
+object timerPaused extends TimerState()
+{
+ shared actual String string = "paused";
+}
+
+"Timer completed state."
+by( "Lis" )
+object timerCompleted extends TimerState()
+{
+ shared actual String string = "completed";
+}
diff --git a/source/herd/schedule/chime/cron/CronExpression.ceylon b/source/herd/schedule/chime/cron/CronExpression.ceylon
new file mode 100644
index 0000000..6e783f3
--- /dev/null
+++ b/source/herd/schedule/chime/cron/CronExpression.ceylon
@@ -0,0 +1,13 @@
+
+"Cron expression parsed by items."
+by( "Lis" )
+shared class CronExpression (
+ "Set of exoression seconds, 0-59." shared Set seconds,
+ "Set of exoression minutes, 0-59." shared Set minutes,
+ "Set of exoression hours, 0-23." shared Set hours,
+ "Set of exoression days of month, 1-31." shared Set daysOfMonth,
+ "Set of exoression months, 1-12." shared Set months,
+ "Days of week." shared DaysOfWeekList daysOfWeek,
+ "Set of exoression years, can be empty." shared Set years
+ )
+{}
diff --git a/source/herd/schedule/chime/cron/DaysOrder.ceylon b/source/herd/schedule/chime/cron/DaysOrder.ceylon
new file mode 100644
index 0000000..7b8a315
--- /dev/null
+++ b/source/herd/schedule/chime/cron/DaysOrder.ceylon
@@ -0,0 +1,62 @@
+import ceylon.time {
+
+ Date
+}
+import ceylon.time.base {
+
+ DayOfWeek
+}
+
+
+"Checks order of day of week."
+by( "Lis" )
+shared interface DayOrder
+{
+ "`true` if data falls on a one of ordered day and `false` otherwise."
+ shared formal Boolean falls( Date date );
+}
+
+
+"Cheks if date falls on a one from day of week list."
+by( "Lis" )
+shared class DaysOfWeekList( "set of day order" shared {DayOrder*} orderedDays ) satisfies DayOrder
+{
+ "`true` if one of ordered days returns `true` and `false` if all of them returns `false`."
+ shared actual Boolean falls( Date date ) {
+ for ( item in orderedDays ) {
+ if ( item.falls( date ) ) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
+
+
+"All days are accepted."
+class DayOrderAll() satisfies DayOrder
+{
+ shared actual Boolean falls( Date date ) => true;
+}
+
+"Checks if data falls on one of specified day of week."
+class DayOrderWeek( "Set of accepted days of week, if empty all days are rejected." shared Set daysOfWeek )
+ satisfies DayOrder
+{
+ shared actual Boolean falls( Date date ) => daysOfWeek.contains( date.dayOfWeek );
+}
+
+"Checks if date is nth day of week."
+class DayOrderNth( "Accepted day of week." shared DayOfWeek day, "'nth' order of day of week." Integer order )
+ satisfies DayOrder
+{
+ shared actual Boolean falls( Date date ) => ( date.dayOfWeek == day ) && ( order == date.day / 7 + 1 );
+}
+
+"Checks if date is last day of week in the month."
+class DayOrderLast( "Accepted day of week." shared DayOfWeek day )
+ satisfies DayOrder
+{
+ shared actual Boolean falls( Date date ) => ( date.dayOfWeek == day ) && ( date.plusDays( 7 ).month != date.month );
+}
+
diff --git a/source/herd/schedule/chime/cron/calendar.ceylon b/source/herd/schedule/chime/cron/calendar.ceylon
new file mode 100644
index 0000000..7fbf33e
--- /dev/null
+++ b/source/herd/schedule/chime/cron/calendar.ceylon
@@ -0,0 +1,101 @@
+
+
+"Defines calendar constants."
+by( "Lis" )
+shared object calendar
+{
+
+ // description fields
+
+ // cron and date fields
+ shared String seconds = "seconds";
+ shared String minutes = "minutes";
+ shared String hours = "hours";
+ shared String daysOfWeek = "days of week";
+
+ shared String daysOfMonth = "days of month";
+ shared String months = "months";
+ shared String years = "years";
+
+ shared String dayOfMonth = "day of month";
+ shared String month = "month";
+ shared String year = "year";
+
+ // name to id maps
+
+ "mapping of month name to month id"
+ shared Map monthShortMap =
+ map {
+ "JAN" -> 1,
+ "FEB" -> 2,
+ "MAR" -> 3,
+ "APR" -> 4,
+ "MAY" -> 5,
+ "JUN" -> 6,
+ "JUL" -> 7,
+ "AUG" -> 8,
+ "SEP" -> 9,
+ "OCT" -> 10,
+ "NOV" -> 11,
+ "DEC" -> 12
+ };
+
+ shared Map monthFullMap =
+ map {
+ "JANUARY" -> 1,
+ "FEBRUARY" -> 2,
+ "MARCH" -> 3,
+ "APRIL" -> 4,
+ "MAY" -> 5,
+ "JUNE" -> 6,
+ "JULY" -> 7,
+ "AUGUST" -> 8,
+ "SEPTEMBER" -> 9,
+ "OCTOBER" -> 10,
+ "NOVEMBER" -> 11,
+ "DECEMBER" -> 12
+ };
+
+ "mapping of day name to day id"
+ shared Map dayOfWeekShortMap =
+ map {
+ "SUN" -> 1,
+ "MON" -> 2,
+ "TUE" -> 3,
+ "WED" -> 4,
+ "THU" -> 5,
+ "FRI" -> 6,
+ "SAT" -> 7
+ };
+
+ "mapping of day name to day id"
+ shared Map dayOfWeekFullMap =
+ map {
+ "SUNDAY" -> 1,
+ "MONDAY" -> 2,
+ "TUESDAY" -> 3,
+ "WEDNESDAY" -> 4,
+ "THURSDAY" -> 5,
+ "FRIDAY" -> 6,
+ "SATURDAY" -> 7
+ };
+
+
+ String replaceStringToNumber( String expression, Map map ) {
+ variable String ret = expression;
+ for ( key -> item in map ) {
+ ret = ret.replace( key, item.string );
+ }
+ return ret;
+ }
+
+ "Replace all occurancies of month names by corresponding number."
+ shared String replaceMonthByNumber( String expression )
+ => replaceStringToNumber( replaceStringToNumber( expression, monthFullMap ), monthShortMap );
+
+ "Replace all occurancies of weekday names by corresponding number."
+ shared String replaceDayOfWeekByNumber( String expression )
+ => replaceStringToNumber( replaceStringToNumber( expression, dayOfWeekFullMap ), dayOfWeekShortMap );
+
+
+}
diff --git a/source/herd/schedule/chime/cron/cronDefinitions.ceylon b/source/herd/schedule/chime/cron/cronDefinitions.ceylon
new file mode 100644
index 0000000..8f4bdc3
--- /dev/null
+++ b/source/herd/schedule/chime/cron/cronDefinitions.ceylon
@@ -0,0 +1,33 @@
+
+"defines some constant used within cron expresion"
+by( "Lis" )
+object cron
+{
+
+ // cron special symbols
+
+ "separators"
+ shared {Character*} separators = { ' ', '\t', '\r', '\n' };
+
+ "delimiter of fields"
+ shared Character delimiter = ',';
+
+ "special symbols in a one token"
+ shared {Character*} special = { '/', '-' };
+
+ "increments symbol"
+ shared Character increments = '/';
+
+ "range symbol"
+ shared Character range = '-';
+
+ "all values symbol"
+ shared Character allValues = '*';
+
+ "last symbol"
+ shared Character last = 'L';
+
+ "nth day symbol"
+ shared Character nth = '#';
+
+}
diff --git a/source/herd/schedule/chime/cron/package.ceylon b/source/herd/schedule/chime/cron/package.ceylon
new file mode 100644
index 0000000..d07c673
--- /dev/null
+++ b/source/herd/schedule/chime/cron/package.ceylon
@@ -0,0 +1,3 @@
+"Parsing cron strings."
+by( "Lis" )
+package herd.schedule.chime.cron;
diff --git a/source/herd/schedule/chime/cron/parseCron.ceylon b/source/herd/schedule/chime/cron/parseCron.ceylon
new file mode 100644
index 0000000..362f860
--- /dev/null
+++ b/source/herd/schedule/chime/cron/parseCron.ceylon
@@ -0,0 +1,67 @@
+import ceylon.time {
+
+ today
+}
+
+
+"Parses cron expression from strings."
+by( "Lis" )
+shared CronExpression? parseCron (
+ "cron style string with seconds" String seconds,
+ "cron style string with minutes" String minutes,
+ "cron style string with hours" String hours,
+ "cron style string with daysOfMonth" String daysOfMonth,
+ "cron style string with months" String months,
+ "cron style string with days of week - optional if not specified all weekdays included" String? daysOfWeek = null,
+ "cron style string with years - optional if not specified every year included" String? years = null,
+ "maximum year period." Integer maxYearPeriod = 10
+) {
+
+ // replace month names by numbers
+ variable String monthsToInt = calendar.replaceMonthByNumber( months.trimmed.uppercased );
+
+ // parse mandatory fields
+ value secondsSet = parseCronStyle( seconds, 0, 59 );
+ value minutesSet = parseCronStyle( minutes, 0, 59 );
+ value hoursSet = parseCronStyle( hours, 0, 23 );
+ value daysOfMonthSet = parseCronStyle( daysOfMonth, 1, 31 );
+ value monthsSet = parseCronStyle( monthsToInt, 1, 12 );
+
+ if ( !secondsSet.empty && !minutesSet.empty && !hoursSet.empty && !daysOfMonthSet.empty && !monthsSet.empty ) {
+
+ // parse days of week, which is nonmandatory, if doesn't exists all days accepted
+ DaysOfWeekList daysOfWeekList;
+ if ( exists strDaysOfWeek = daysOfWeek ) {
+ // replace all weekday names by numbers
+ variable String weekdayToInt = calendar.replaceDayOfWeekByNumber( strDaysOfWeek.trimmed.uppercased );
+ // do parsing
+ if ( exists parsedDaysOfWeek = parseCronDaysOfWeek( weekdayToInt ) ) {
+ daysOfWeekList = parsedDaysOfWeek;
+ }
+ else {
+ return null;
+ }
+ }
+ else {
+ daysOfWeekList = DaysOfWeekList( {DayOrderAll()} );
+ }
+
+ // parse years, which is nonmandatory, if doesn't exists any year accepted
+ Set yearsSet;
+ if ( exists strYears = years ) {
+ Integer todayYear = today().year;
+ yearsSet = parseCronStyle( strYears, todayYear, todayYear + maxYearPeriod );
+ if ( yearsSet.empty ) {
+ return null;
+ }
+ }
+ else {
+ yearsSet = emptySet;
+ }
+
+ return CronExpression( secondsSet, minutesSet, hoursSet, daysOfMonthSet, monthsSet, daysOfWeekList, yearsSet );
+ }
+ else {
+ return null;
+ }
+}
diff --git a/source/herd/schedule/chime/cron/parseCronDaysOfWeek.ceylon b/source/herd/schedule/chime/cron/parseCronDaysOfWeek.ceylon
new file mode 100644
index 0000000..a25d812
--- /dev/null
+++ b/source/herd/schedule/chime/cron/parseCronDaysOfWeek.ceylon
@@ -0,0 +1,77 @@
+import ceylon.collection {
+
+ ArrayList,
+ HashSet,
+ linked,
+ Hashtable
+}
+import ceylon.time.base {
+
+ dayOfWeek,
+ DayOfWeek
+}
+
+
+"Parses days of week from string."
+by( "Lis" )
+DaysOfWeekList? parseCronDaysOfWeek( String expression ) {
+
+ // all values
+ if ( expression == cron.allValues.string || expression.empty ) {
+ return DaysOfWeekList( {DayOrderAll()} );
+ }
+
+ ArrayList days = ArrayList();
+ // parse tokens
+ {String*} tokens = expression.split( cron.delimiter.equals ).map( String.trimmed );
+ for ( token in tokens ) {
+ if ( exists parsedToken = parseDayOrder( token ) ) {
+ days.add( parsedToken );
+ }
+ else {
+ return null;
+ }
+ }
+
+ if ( days.empty ) {
+ return null;
+ }
+ else {
+ return DaysOfWeekList( { for ( item in days ) item } );
+ }
+
+}
+
+
+"Parses day order from string."
+by( "Lis" )
+DayOrder? parseDayOrder( String expression ) {
+ if ( expression.contains( cron.nth ) ) {
+ {String*} tokens = expression.split( cron.nth.equals );
+ if ( tokens.size == 2,
+ exists weekday = parseStringToInteger( tokens.first ),
+ exists order = parseStringToInteger( tokens.last )
+ ) {
+ if ( weekday > 0 && weekday < 8 && order > 0 && order < 6 ) {
+ return DayOrderNth( dayOfWeek( weekday - 1 ), order );
+ }
+ }
+ }
+ else if ( exists last = expression.last, last == cron.last ) {
+ if ( exists day = parseStringToInteger( expression.spanTo( expression.size - 2 ) ) ) {
+ if ( day > 0 && day < 8 ) {
+ return DayOrderLast( dayOfWeek( day - 1 ) );
+ }
+ }
+ }
+ else {
+ if ( exists daysSet = parseCronRange( expression, 1, 7 ) ) {
+ return DayOrderWeek (
+ HashSet (
+ linked, Hashtable(), daysSet.map( ( Integer element ) => dayOfWeek( element - 1 ) )
+ )
+ );
+ }
+ }
+ return null;
+}
diff --git a/source/herd/schedule/chime/cron/parseCronRange.ceylon b/source/herd/schedule/chime/cron/parseCronRange.ceylon
new file mode 100644
index 0000000..09d4c5e
--- /dev/null
+++ b/source/herd/schedule/chime/cron/parseCronRange.ceylon
@@ -0,0 +1,115 @@
+import ceylon.collection {
+
+ HashSet
+}
+
+
+"Parses cron range in format TO-FROM / STEP
+ where TO, FROM and STEP:
+ * contains only digits,
+ * `FROM`-`TO`/`STEP`, `FROM`, `TO` and `STEP` are digits
+ * `FROM`/`STEP`, `FROM` and `STEP` are digits, TO eqauls to max possible value
+ * `FROM`-`TO`, FROM and TO are digits, step is supposed to be 1
+ * TO and FROM have to be greater or equal min and less or equal max
+"
+by( "Lis" )
+Set? parseCronRange( String expression, Integer minValue, Integer maxValue ) {
+ HashSet ret = HashSet();
+ {String*} ranged = expression.split( cron.special.contains, false ).map( String.trimmed );
+ if ( exists from = parseStringToInteger( ranged.first ) ) {
+ variable Integer to = from;
+ variable Integer step = 1;
+ if ( ranged.size == 1 ) {
+ if ( from < minValue || from > maxValue ) {
+ // 'from' to be within accepted values
+ return null;
+ }
+ }
+ else if ( ranged.size == 3 ) {
+ if ( exists del = ranged.getFromFirst( 1 ) ) {
+ if ( del == cron.range.string ) {
+ if ( exists parsed = parseStringToInteger( ranged.getFromFirst( 2 ) ) ) {
+ to = parsed;
+ if ( to < from || to > maxValue ) {
+ // 'to' to be within accepted values
+ return null;
+ }
+ }
+ else {
+ // not digits
+ return null;
+ }
+ }
+ else if ( del == cron.increments.string ) {
+ if ( exists parsed = parseStringToInteger( ranged.getFromFirst( 2 ) ) ) {
+ if ( parsed < 1 ) {
+ // step to be greater zero
+ return null;
+ }
+ step = parsed;
+ to = maxValue;
+ }
+ else {
+ // not digits
+ return null;
+ }
+ }
+ else {
+ // only '-' or '/' supported
+ return null;
+ }
+ }
+ else {
+ return null;
+ }
+ } else if ( ranged.size == 5 ) {
+ if ( exists del1 = ranged.getFromFirst( 1 ), exists del2 = ranged.getFromFirst( 3 ) ) {
+ if ( del1 == cron.range.string && del2 == cron.increments.string ) {
+ if ( exists parsedTo = parseStringToInteger( ranged.getFromFirst( 2 ) ),
+ exists parsedStep = parseStringToInteger( ranged.getFromFirst( 4 ) ) )
+ {
+ if ( parsedTo < minValue || parsedTo > maxValue || parsedStep < 1 ) {
+ // incorrect values
+ return null;
+ }
+ to = parsedTo;
+ step = parsedStep;
+ }
+ else {
+ // not digits
+ return null;
+ }
+ }
+ else {
+ // incorrect format - to be X-X/X
+ return null;
+ }
+ }
+ else {
+ return null;
+ }
+ }
+ else {
+ // token must be in format X-X/X
+ return null;
+ }
+ if ( step < 1 ) {
+ step = 1;
+ }
+ // store range into set
+ variable Integer storing = from;
+ while ( storing <= to && storing <= maxValue ) {
+ if ( storing >= minValue ) {
+ ret.add( storing );
+ }
+ storing += step;
+ }
+ if ( ret.empty ) {
+ return null;
+ }
+ else {
+ return ret;
+ }
+ }
+ return null;
+}
diff --git a/source/herd/schedule/chime/cron/parseCronStyle.ceylon b/source/herd/schedule/chime/cron/parseCronStyle.ceylon
new file mode 100644
index 0000000..11c4b85
--- /dev/null
+++ b/source/herd/schedule/chime/cron/parseCronStyle.ceylon
@@ -0,0 +1,46 @@
+import ceylon.collection {
+
+ HashSet
+}
+
+
+"Parses string expression with cron style time, returns set with all possible values.
+ Supported time in format X,X,...
+ where X to be:
+ * digits greater or equal min and less or equal max,
+ * `FROM`-`TO`/`STEP`, `FROM`, `TO` and `STEP` are digits
+ * `FROM`/`STEP`, `FROM` and `STEP` are digits, TO eqauls to max possible value
+ * `FROM`-`TO`, FROM and TO are digits, step is supposed to be 1
+ "
+by( "Lis" )
+Set parseCronStyle(
+ "expression to be parsed" String expression,
+ "min possible value" Integer minValue,
+ "max possible value" Integer maxValue )
+{
+ String trimmedExpr = expression.trimmed;
+ HashSet ret = HashSet();
+
+ // all values
+ if ( trimmedExpr == cron.allValues.string ) {
+ variable Integer storing = minValue;
+ while ( storing <= maxValue ) {
+ ret.add( storing );
+ storing ++;
+ }
+ return ret;
+ }
+
+ // parse tokens
+ {String*} tokens = trimmedExpr.split( cron.delimiter.equals ).map( String.trimmed );
+ for ( token in tokens ) {
+ if ( exists tokenSet = parseCronRange( token, minValue, maxValue ) ) {
+ ret.addAll( tokenSet );
+ }
+ else {
+ ret.clear();
+ break;
+ }
+ }
+ return ret;
+}
diff --git a/source/herd/schedule/chime/cron/stringToInteger.ceylon b/source/herd/schedule/chime/cron/stringToInteger.ceylon
new file mode 100644
index 0000000..100535c
--- /dev/null
+++ b/source/herd/schedule/chime/cron/stringToInteger.ceylon
@@ -0,0 +1,10 @@
+
+"Parses Integer from String?."
+Integer? parseStringToInteger( String? str ) {
+ if ( exists parsing = str ) {
+ return parseInteger( parsing );
+ }
+ else {
+ return null;
+ }
+}
diff --git a/source/herd/schedule/chime/errorMessages.ceylon b/source/herd/schedule/chime/errorMessages.ceylon
new file mode 100644
index 0000000..7d8c9a8
--- /dev/null
+++ b/source/herd/schedule/chime/errorMessages.ceylon
@@ -0,0 +1,48 @@
+
+"Defines error messages."
+by( "Lis" )
+shared object errorMessages {
+
+ shared String unsupportedOperation = "unsupported operation";
+
+ shared String operationIsNotSpecified = "operation has to be specified";
+
+
+ shared String schedulerNotExists = "scheduler doesn't exist";
+
+ shared String schedulerNameHasToBeSpecified = "scheduler name has to be specified";
+
+ shared String incorrectSchedulerState = "scheduler state has to be one of - 'get', 'paused', 'running'";
+
+ shared String stateToBeSpecified = "state has to be specified";
+
+
+ shared String timerAlreadyExists = "timer already exists";
+
+ shared String timerNotExists = "timer doesn't exist";
+
+ shared String timerNameHasToBeSpecified = "timer name has to be specified";
+
+ shared String timerTypeHasToBeSpecified = "timer type has to be specified";
+
+ shared String unsupportedTimerType = "unsupported timer type";
+
+ shared String incorrectStartDate = "incorrect start date";
+
+ shared String incorrectEndDate = "incorrect end date";
+
+ shared String endDateToBeAfterStartDate = "end date has to be after start date";
+
+ shared String unsupportedTimezone = "unsupported time zone";
+
+ shared String timerDescriptionHasToBeSpecified = "timer description has to be specified";
+
+ shared String incorrectTimerState = "timer state has to be one of - 'get', 'paused', 'running'";
+
+ shared String intervalHasToBeSpecified = "interval has to be specified";
+
+ shared String intervalHasToBeGreaterThanZero = "interval has to be greater than zero";
+
+ shared String incorrectCronTimerDescription = "incorrect cron timer description";
+
+}
diff --git a/source/herd/schedule/chime/module.ceylon b/source/herd/schedule/chime/module.ceylon
new file mode 100644
index 0000000..1109f61
--- /dev/null
+++ b/source/herd/schedule/chime/module.ceylon
@@ -0,0 +1,401 @@
+
+"
+ _Chime_ is time scheduler which works on _Vert.x_ event bus and provides:
+ * scheduling with _cron-style_ and _interval_ timers
+ * applying time zones available on _JVM_
+
+ >Compiled for Ceylon 1.2.0 and Vert.x 3.2.1
+
+
+ ## Running.
+
+ Deploy _Chime_ using `Vertx.deployVerticle` method.
+
+ import io.vertx.ceylon.core { vertx }
+
+ vertx.vertx().deployVerticle (
+ \"ceylon:herd.schedule.chime/0.1.0\",
+ (String|Throwable res) {
+ ...
+ }
+ );
+
+
+ >_Chime_ exchanges events with customers via event bus with `JSON` messages.
+
+
+ ## Configuration.
+
+ Following parameters could be specified in `JSON` verticle configuration:
+
+ * \"address\" -> address _Chime_ is listen to, `String`, default is \"chime\"
+ * \"max year period limit\" -> limiting scheduling period in years, `Integer`, default is 10 years
+ * \"tolerance\" -> tolerance in milliseconds used to compare actual and requested times,
+ `Integer`, default is 10 milliseconds
+
+
+ ## Scheduling.
+
+ _Chime_ operates by two structures: _timer_ and _scheduler_.
+ _Scheduler_ is a set or group of timers. At least one _scheduler_ has to be created before creating _timers_.
+
+
+ ### _Scheduler_.
+
+ ##### Scheduler messages.
+
+ In order to maintain _schedulers_ send `JSON` message on _Chime_ address (specified in configuration, \"chime\" is default)
+ in the following format:
+ {
+ \"operation\" -> String // operation code, mandatory
+ \"name\" -> String // scheduler name, mandatory
+ \"state\" -> String // state, mandatory only if operation = 'state'
+ }
+
+ >_Chime_ listens event bus on \"scheduler name\" address with messages for the given _scheduler_.
+
+
+ ##### Scheduler operation codes.
+
+ * \"create\" - create new scheduler with specified name, state and description,
+ if state is not specified, scheduler is put to running state.
+ * \"delete\" - delete scheduler with name `name`. All timers within _scheduler_ will be canceled.
+ * \"info\" - requesting info on _Chime_ or specific scheduler (scheduler name to be provided)
+ * \"state\":
+ * if is \"get\" state is to be returned
+ * if is \"running\" scheduler is to be set to _running_, which leads all non paused timers are _running_
+ * if is \"paused\" scheduler is to be set to _paused_, which leads all timers are _paused_
+ * otherwise error is returned
+
+
+ ##### Scheduler examples.
+
+ // create new scheduler with name \"scheduler name\"
+ JSON message = JSON {
+ \"operation\" -> \"create\",
+ \"name\" -> \"scheduler name\"
+ }
+
+ // change state of scheduler with \"scheduler name\" on paused
+ JSON message = JSON {
+ \"operation\" -> \"state\",
+ \"name\" -> \"scheduler name\",
+ \"state\" -> \"paused\"
+ }
+
+
+ ##### Scheduler response.
+
+ _Chime_ responds on messages in `JSON` format:
+ {
+ \"response\" -> String // response code - one of \"ok\" or \"error\"
+ \"name\" -> String // scheduler name
+ \"state\" -> String // scheduler state
+ \"schedulers\" -> JSONArray // scheduler names, exists as response on \"info\" operation with no \"name\" field
+ \"error\" -> String // error description, exists only if response == \"error\"
+ }
+
+
+ ### _Timer_.
+
+ Once _shceduler_ is created _timers_ can be run within.
+
+ There are two ways to access specific timer:
+ * sending message on \"scheduler name\" address using timer short name \"timer name\"
+ * sending message on _Chime_ address using full timer name which is \"scheduler name:timer name\"
+
+
+ >Timer full name is _scheduler name_ and _timer name_ separated with ':', i.e. \"scheduler name:timer name\".
+
+
+ >Request on _Chime_ address with _timer full name_ and request on _scheduler_ address with timer full or short name
+ are equivalent.
+
+
+ ##### Timer request.
+
+ Request has to be sent in `JSON` format on _scheduler name_ address with _timer short name_
+ or on _Chime_ address with _timer full name_.
+ Request format:
+
+ {
+ \"operation\" -> String // operation code, mandatory
+ \"name\" -> String // timer short or full name, mandatory
+ \"state\" -> String // state, nonmandatory, except if operation = 'sate'
+
+ // fields for create operation:
+ \"maximum count\" -> Integer // maximum number of fires, default - unlimited
+ \"publish\" -> Boolean // if true message to be published and to be sent otherwise, nonmandatory
+
+ \"start time\" -> JSON // start time, nonmadatory, if doesn't exist timer will start immediately
+ {
+ \"seconds\" -> Integer // seconds, mandatory
+ \"minutes\" -> Integer // minutes, mandatory
+ \"hours\" -> Integer // hours, mandatory
+ \"day of month\" -> Integer // days of month, mandatory
+ \"month\" -> Integer or String // months, mandatory
+ \"year\" -> Integer // year, mandatory
+ }
+
+ \"end time\" -> `JSON` // end time, nonmadatory, default no end time
+ {
+ \"seconds\" -> Integer // seconds, mandatory
+ \"minutes\" -> Integer // minutes, mandatory
+ \"hours\" -> Integer // hours, mandatory
+ \"day of month\" -> Integer // days of month, mandatory
+ \"month\" -> Integer or String // months, mandatory
+ \"year\" -> Integer // year, mandatory
+ }
+
+ \"time zone\" -> String // time zone ID, nonmandatory, default server local
+
+ \"description\" -> JSON // timer desciption, mandatoty for create operation
+ }
+
+
+ >_Chime_ address could be specified in `verticle` configuration, default is \"chime\".
+
+
+ ##### Timer operation codes.
+
+ * \"create\" - create new timer with specified name, state and description
+ * \"delete\" - delete timer with name `name`
+ * \"info\" - get information on timer (if timer name is specified) or scheduler (if timer name is not specified)
+ * \"state\":
+ * if state field is \"get\" timer state is to be returned
+ * if state field is \"running\" timer state is to be set to _running_
+ * if state field is \"paused\" timer state is to be set to _paused_
+ * otherwise error is returned
+
+ >Timer fires only if both _timer_ and _scheduler_ states are _running_.
+
+
+ ##### Supported timers.
+
+ Timer is specified within _description_ field of timer creation request.
+
+ * __Cron style timer__. Timer which is defined like _cron_, but with some simplifications
+ {
+ \"type\" -> \"cron\" // timer type, mandatory
+
+ \"seconds\" -> String // seconds in cron style, mandatory
+ \"minutes\" -> String // minutes in cron style, mandatory
+ \"hours\" -> String // hours in cron style, mandatory
+ \"days of month\" -> String // days of month in cron style, mandatory
+ \"months\" -> String // months in cron style, mandatory
+ \"days of week\" -> String // days of week in cron style, L means last, # means nth of month, nonmandatory
+ \"years\" -> String // year in cron style, nonmandatory
+ }
+ All fields can be specified using following notations:
+ * `FROM`-`TO`/`STEP`
+ * `FROM`/`STEP`
+ * `FROM`-`TO`
+ * '*' means any allowed
+ * month can be specified using digits (1 is for January) or using names (like 'jan' or 'january', case insensitive)
+ * day of week can be specified using digits (1 is for Sunday) or using names (like 'sun' or 'sunday', case insensitive)
+
+ ------------------------------------------
+
+ * __Interval timer__. Timer which fires after each given time period (minimum 1 second)
+ {
+ \"type\" -> \"interval\" // timer type, mandatory
+ \"increment seconds\" -> Integer // increment in seconds, if <= 0 timer fires only once, mandatory
+ }
+
+
+ ##### Scheduler response on timer request.
+
+ _Chime_ responds on each request in `JSON` format:
+ {
+ \"response\" -> String // response code - one of `ok` or `error`
+ \"name\" -> String // timer name
+ \"state\" -> String // state
+
+ \"error\" -> String // error description, exists only if response == `error`
+ \"timers\" -> JSONArray // list of timer names currently scheduled - response on info operation with no name field specified
+
+ // 'Info' operation returns fields from 'create' operation
+ }
+
+
+ ##### Timer firing.
+
+ When _timer_ fires it sends or publishes `JSON` message on _full timer name_ address in the following format:
+
+ {
+ \"name\" -> String, timer name
+ \"count\" -> Integer, total number of fire times
+ \"state\" -> String, timer state, one of 'running', 'paused' or 'completed'
+ \"time\" -> String formated time / date
+ \"seconds\" -> Integer, number of seconds since last minute
+ \"minutes\" -> Integer, number of minutes since last hour
+ \"hours\" -> Integer, hour of day
+ \"day of month\" -> Integer, day of month
+ \"month\" -> Integer, month
+ \"year\" -> Integer, year
+ \"time zone\" -> String, time zone ID
+ }
+
+
+ >_Timer full name_ is _scheduler name_ and _timer name_ separated with ':', i.e. \"scheduler name:timer name\".
+
+
+ >Timer _sends_ or _publishes_ message depending on \"publish\" field in timer description (passed at timer creation request).
+
+
+ >String formatted time / date is per [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601).
+
+
+ ##### Time zones.
+
+ [list of available time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones),
+ actual availability may depend on particular JVM installation.
+
+ see also [time zones and JRE](http://www.oracle.com/technetwork/java/javase/dst-faq-138158.html).
+
+
+ ##### Timer example.
+
+ eventBus.send (
+ \"chime\",
+ JSON {
+ \"operation\" -> \"create\",
+ \"name\" -> \"schedule manager\",
+ \"state\" -> \"running\"
+ },
+ (Throwable|Message msg) {
+ if (is Message msg) {
+ eventBus.send(
+ \"chime\",
+ JSON {
+ \"operation\" -> \"create\",
+ \"name\" -> \"schedule manager:schedule timer\",
+ \"state\" -> \"running\",
+ \"publish\" -> false,
+ \"max count\" -> 3,
+ \"time zone\" -> \"Europe/Paris\",
+ \"descirption\" -> JSON {
+ \"type\" -> \"cron\", // timer type is 'cron'
+ \"seconds\" -> \"27/30\", // 27 with step 30 leads to fire at 27 and 57 seconds
+ \"minutes\" -> \"*\", // every minute
+ \"hours\" -> \"0-23\", // every hour
+ \"days of month\" -> \"1-31\", // every day
+ \"months\" -> \"january-OCTOBER\", // from January and up to October
+ \"days of week\" -> \"sat#2,sunday\", // at second Saturday and at each Sunday
+ \"years\" -> \"2015-2019\"
+ }
+ },
+ (Throwable|Message msg) {
+ print(msg);
+ }
+ );
+ }
+ else {
+ print(\"time scheduler creation error: \`\`msg\`\`\");
+ }
+ }
+ );
+
+
+ ### Error messages.
+
+ When error occured _Chime_ replies on corresponding message with error:
+ {
+ \"response\" -> \"error\"
+ \"error\" -> String with error description
+ }
+
+ possible errors (see [[value errorMessages]]):
+ * \"unsupported operation\"
+ * \"operation has to be specified\"
+ * \"scheduler doesn't exist\"
+ * \"scheduler name has to be specified\"
+ * \"scheduler state has to be one of - 'get', 'paused', 'running'\"
+ * \"state has to be specified\"
+ * \"timer already exists\"
+ * \"timer doesn't exist\"
+ * \"timer name has to be specified\"
+ * \"timer type has to be specified\"
+ * \"unsupported timer type\"
+ * \"incorrect start date\"
+ * \"incorrect end date\"
+ * \"end date has to be after start date\"
+ * \"unsupported time zone\"
+ * \"timer description has to be specified\"
+ * \"timer state has to be one of - 'get', 'paused', 'running'\"
+ * \"interval has to be specified\"
+ * \"interval has to be greater than zero\"
+ * \"incorrect cron timer description\"
+
+
+ ## Cron expressions.
+
+
+ ##### Expression fields.
+
+ * _seconds_, mandatory
+ * allowed values: 0-59
+ * allowed special characters: , - * /
+ * _minutes_, mandatory
+ * allowed values: 0-59
+ * allowed special characters: , - * /
+ * _hours_, mandatory
+ * allowed values: 0-23
+ * allowed special characters: , - * /
+ * _days of month_, mandatory
+ * allowed values 1-31
+ * allowed special characters: , - * /
+ * _months_, mandatory
+ * allowed values 1-12, Jan-Dec, January-December
+ * allowed special characters: , - * /
+ * _days of week_, nonmandatory
+ * allowed values 1-7, Sun-Sat, Sunday-Saturday
+ * allowed special characters: , - * / L #
+ * _years_, nonmandatory
+ * allowed values 1970-2099
+ * allowed special characters: , - * /
+
+
+ >Names of months and days of the week are _not_ case sensitive.
+
+
+ ##### Special characters.
+
+ * '*' means all values
+ * ',' separates list items
+ * '-' specifies range, for example, '10-12' means '10, 11, 12'
+ * '/' specifies increments, for example, '0/15' in seconds field means '0,15,30,45',
+ '0-30/15' means '0,15,30'
+ * 'L' has to be used after digit and means _the last xxx day of the month_,
+ for example, '6L' means _the last friday of the month_
+ * '#' has to be used with digits before and after: 'x#y' and means _the y'th x day of the month_,
+ for example, '6#3' means _the third Friday of the month_
+
+ "
+license (
+ "The MIT License (MIT)
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the \"Software\"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE."
+)
+by( "Lis" )
+native( "jvm" )
+module herd.schedule.chime "0.1.0" {
+ shared import io.vertx.ceylon.core "3.2.1";
+ import ceylon.time "1.2.0";
+}
diff --git a/source/herd/schedule/chime/package.ceylon b/source/herd/schedule/chime/package.ceylon
new file mode 100644
index 0000000..04a25dd
--- /dev/null
+++ b/source/herd/schedule/chime/package.ceylon
@@ -0,0 +1 @@
+shared package herd.schedule.chime;
diff --git a/source/herd/schedule/chime/timer/StandardTimerFactory.ceylon b/source/herd/schedule/chime/timer/StandardTimerFactory.ceylon
new file mode 100644
index 0000000..799f0c6
--- /dev/null
+++ b/source/herd/schedule/chime/timer/StandardTimerFactory.ceylon
@@ -0,0 +1,89 @@
+import ceylon.json {
+
+ JSON=Object
+}
+import herd.schedule.chime.cron {
+
+ calendar,
+ parseCron
+}
+import herd.schedule.chime {
+
+ errorMessages
+}
+
+
+"Standard time factory. Creates:
+ * cron-like timer [[herd.schedule.chime.timer::TimerCronStyle]]
+ * incremental timer [[herd.schedule.chime.timer::TimerInterval]]
+ "
+shared class StandardTimerFactory( "max year limitation" Integer maxYearPeriod = 10 ) extends FactoryJSONBase()
+ satisfies TimerFactory
+ {
+
+
+ "Initializes factory - to be called before using (creators adding is performed here)."
+ shared TimerFactory initialize() {
+ addCreator( timerDefinitions.typeCronStyle, createCronTimer );
+ addCreator( timerDefinitions.typeInterval, createIntervalTimer );
+ return this;
+ }
+
+
+ // timer creators
+
+ "Creates cron style timer."
+ Timer|String createCronTimer( "Timer description." JSON description ) {
+ if ( is String seconds = description.get( calendar.seconds ),
+ is String minutes = description.get( calendar.minutes ),
+ is String hours = description.get( calendar.hours ),
+ is String daysOfMonth = description.get( calendar.daysOfMonth ),
+ is String months = description.get( calendar.months )
+ ) {
+ // days of week - nonmandatory
+ String? daysOfWeek;
+ if ( is String str = description.get( calendar.daysOfWeek ) ) {
+ daysOfWeek = str;
+ }
+ else {
+ daysOfWeek = null;
+ }
+
+ // years - nonmandatory
+ String? years;
+ if ( is String str = description.get( calendar.years ) ) {
+ years = str;
+ }
+ else {
+ years = null;
+ }
+
+ if ( exists cronExpr = parseCron( seconds, minutes, hours, daysOfMonth, months, daysOfWeek, years, maxYearPeriod ) ) {
+ return TimerCronStyle( cronExpr );
+ }
+ else {
+ return errorMessages.incorrectCronTimerDescription;
+ }
+
+ }
+ else {
+ return errorMessages.incorrectCronTimerDescription;
+ }
+ }
+
+
+ "Creates interval timer."
+ Timer|String createIntervalTimer( "Timer description." JSON description ) {
+ if ( is Integer interval = description.get( timerDefinitions.intervalSeconds ) ) {
+
+ if ( interval > 0 ) {
+ return TimerInterval( interval * 1000 );
+ }
+ else {
+ return errorMessages.intervalHasToBeGreaterThanZero;
+ }
+ }
+ return errorMessages.intervalHasToBeSpecified;
+ }
+
+}
diff --git a/source/herd/schedule/chime/timer/Timer.ceylon b/source/herd/schedule/chime/timer/Timer.ceylon
new file mode 100644
index 0000000..48f4433
--- /dev/null
+++ b/source/herd/schedule/chime/timer/Timer.ceylon
@@ -0,0 +1,20 @@
+import ceylon.time {
+
+ DateTime
+}
+
+
+"Timer interface."
+by( "Lis" )
+shared interface Timer
+{
+
+ "Starts the timer using [[current]] time.
+ Returns next fire time if successfull or null if completed."
+ shared formal DateTime? start( "current time" DateTime current );
+
+ "Shifts time to next one.
+ Returns next fire time if successfull and null if completed."
+ shared formal DateTime? shiftTime();
+
+}
diff --git a/source/herd/schedule/chime/timer/TimerCronStyle.ceylon b/source/herd/schedule/chime/timer/TimerCronStyle.ceylon
new file mode 100644
index 0000000..7d2af37
--- /dev/null
+++ b/source/herd/schedule/chime/timer/TimerCronStyle.ceylon
@@ -0,0 +1,443 @@
+import ceylon.time {
+
+ DateTime,
+ dateTime
+}
+
+import ceylon.time.chronology {
+
+ gregorian
+}
+import herd.schedule.chime.cron {
+
+ CronExpression
+}
+
+
+"Ccron-like timer."
+by( "Lis" )
+class TimerCronStyle (
+ "Cron expression rules the timer." CronExpression expression
+)
+ satisfies Timer
+{
+
+ // indexies of current time
+ variable Iterator secondIndex = expression.seconds.iterator();
+ variable Iterator minuteIndex = expression.minutes.iterator();
+ variable Iterator hourIndex = expression.hours.iterator();
+ variable Iterator dayIndex = expression.daysOfMonth.iterator();
+ variable Iterator monthIndex = expression.months.iterator();
+ variable Iterator yearIndex = expression.years.iterator();
+
+ // current time
+ variable Integer currentYear = 0;
+ variable Integer currentMonth = 1;
+ variable Integer currentDay = 1;
+ variable Integer currentHour = 0;
+ variable Integer currentMinute = 0;
+ variable Integer currentSecond = 0;
+
+ "current date and time"
+ variable DateTime currentDate = dateTime( 0, 1, 1 );
+
+ variable Boolean completed = false;
+
+
+ "Completes the timer."
+ void completeTimer() {
+ completed = true;
+ currentDate = dateTime( 0, 1, 1 );
+ secondIndex = expression.seconds.iterator();
+ minuteIndex = expression.minutes.iterator();
+ hourIndex = expression.hours.iterator();
+ dayIndex = expression.daysOfMonth.iterator();
+ monthIndex = expression.months.iterator();
+ yearIndex = expression.years.iterator();
+ }
+
+ "Starts seconds from beginning."
+ void resetSeconds() {
+ secondIndex = expression.seconds.iterator();
+ if ( is Integer val = secondIndex.next() ){
+ currentSecond = val;
+ }
+ else {
+ currentSecond = 0;
+ }
+ }
+
+ "Starts minutes from beginning and reset seconds."
+ void resetMinutes() {
+ minuteIndex = expression.minutes.iterator();
+ if ( is Integer val = minuteIndex.next() ){
+ currentMinute = val;
+ }
+ else {
+ currentMinute = 0;
+ }
+ resetSeconds();
+ }
+
+ "Starts hours from beginning and reset minutes."
+ void resetHours() {
+ hourIndex = expression.hours.iterator();
+ if ( is Integer val = hourIndex.next() ){
+ currentHour = val;
+ }
+ else {
+ currentHour = 0;
+ }
+ resetMinutes();
+ }
+
+ "Start. days from beginning and reset hours."
+ void resetDays() {
+ dayIndex = expression.daysOfMonth.iterator();
+ if ( is Integer val = dayIndex.next() ){
+ currentDay = val;
+ }
+ else {
+ currentDay = 1;
+ }
+ resetHours();
+ }
+
+ "Starts days from beginning and reset and reset days."
+ void resetMonth() {
+ monthIndex = expression.months.iterator();
+ if ( is Integer val = monthIndex.next() ){
+ currentMonth = val;
+ }
+ else {
+ currentMonth = 1;
+ }
+ resetDays();
+ }
+
+ "Returns `true` if data is accepted and `false` otherwise."
+ Boolean isDateAcepted() {
+ value converted = gregorian.dateFrom( gregorian.fixedFrom( [currentYear, currentMonth, currentDay] ) );
+ return converted[0] == currentYear && converted[1] == currentMonth && converted[2] == currentDay;
+ }
+
+ "Shifts year to next after the latest fire. Next year is one from specified in [[CronExpression.years]].
+ If all years scooped out timing is completed.
+ Returns `true` if completed."
+ Boolean shiftYear() {
+ // shift to next year
+ if ( !expression.years.empty ) {
+ if ( is Integer item = yearIndex.next() ) {
+ currentYear = item;
+ }
+ else {
+ return true;
+ }
+ }
+ else {
+ currentYear ++;
+ }
+ return false;
+ }
+
+ "Shifts month to next after the latest fire. Next month is one from specified in [[CronExpression.months]].
+ If all months scooped out shifts year - [[shiftYear]].
+ Returns `true` if completed and `false` otherwise."
+ Boolean shiftMonth() {
+ // shift to next month
+ if ( is Integer item = monthIndex.next() ) {
+ currentMonth = item;
+ if ( isDateAcepted() ) {
+ return false;
+ }
+ else {
+ resetMonth();
+ return shiftYear();
+ }
+ }
+ else {
+ resetMonth();
+ return shiftYear();
+ }
+ }
+
+ "Shifts day to next after the latest fire. Next day is one from specified in [[CronExpression.daysOfMonth]].
+ If all days scooped out shifts month - [[shiftMonth]].
+ Returns `true` if completed and `false` otherwise."
+ Boolean shiftDay() {
+ // shift to next day
+ if ( is Integer item = dayIndex.next() ) {
+ currentDay = item;
+ if ( isDateAcepted() ) {
+ return false;
+ }
+ else {
+ resetDays();
+ return shiftMonth();
+ }
+ }
+ else {
+ resetDays();
+ return shiftMonth();
+ }
+ }
+
+ "Shifts hours to next after the latest fire. Next hours are one from specified in [[CronExpression.hours]].
+ If all hours scooped out shifts day - [[shiftDay]].
+ Returns `true` if completed and `false` otherwise."
+ Boolean shiftHour() {
+ // shift to next hour
+ if ( is Integer item = hourIndex.next() ) {
+ currentHour = item;
+ return false;
+ }
+ else {
+ resetHours();
+ return shiftDay();
+ }
+ }
+
+ "Shifts minutes to next after the latest fire. Next minutes are one from specified in [[CronExpression.minutes]].
+ If all minutes scooped out shifts hours - [[shiftHour]].
+ Returns `true` if completed and `false` otherwise."
+ Boolean shiftMinute() {
+ // shift to next minute
+ if ( is Integer item = minuteIndex.next() ) {
+ currentMinute = item;
+ return false;
+ }
+ else {
+ resetMinutes();
+ return shiftHour();
+ }
+ }
+
+ "Shifts seconds to next after the latest fire. Next seconds are one from specified in [[CronExpression.seconds]].
+ If all seconds scooped out shifts minutes - [[shiftMinute]].
+ Returns `true` if completed and `false` otherwise."
+ Boolean shiftSecond() {
+ if ( is Integer item = secondIndex.next() ) {
+ currentSecond = item;
+ return false;
+ }
+ else {
+ resetSeconds();
+ return shiftMinute();
+ }
+
+ }
+
+ "Considers weekdays in the [[currentDate]]. I.e. shifts days while [[currentDay]] is not within [[CronExpression.daysOfWeek]].
+ Returns `false` if timer to be completed and `true` otherwise."
+ Boolean considerWeekdays() {
+ try {
+ variable DateTime date = dateTime( currentYear, currentMonth, currentDay, currentHour, currentMinute, currentSecond );
+ // shift to appropriate day of week
+ variable Boolean reset = true;
+ while ( !expression.daysOfWeek.falls( date.date ) && date != currentDate ) {
+ if ( reset ) {
+ resetHours();
+ reset = false;
+ }
+ if ( shiftDay() ) {
+ completeTimer();
+ return false;
+ }
+ date = dateTime( currentYear, currentMonth, currentDay, currentHour, currentMinute, currentSecond );
+ }
+ if ( date != currentDate ) {
+ currentDate = date;
+ return true;
+ }
+ else {
+ completeTimer();
+ return false;
+ }
+ }
+ catch ( Throwable err ) {
+ completeTimer();
+ return false;
+ }
+ }
+
+
+ "Starts timing from specified UTC time.
+ Returns `true` if started and `false` if completed."
+ Boolean startCron( DateTime current ) {
+ // find nearest time
+
+ // year
+ if ( expression.years.empty ) {
+ currentYear = current.year;
+ }
+ else {
+ currentYear = 0;
+ yearIndex = expression.years.iterator();
+ while ( is Integer item = yearIndex.next() ) {
+ if ( item >= current.year ) {
+ currentYear = item;
+ break;
+ }
+ }
+ if ( currentYear == 0 ) {
+ completeTimer();
+ return false;
+ }
+ else if ( currentYear > current.year ) {
+ resetMonth();
+ return considerWeekdays();
+ }
+ }
+
+ // month
+ currentMonth = 0;
+ monthIndex = expression.months.iterator();
+ while ( is Integer item = monthIndex.next() ) {
+ if ( item >= current.month.integer ) {
+ currentMonth = item;
+ break;
+ }
+ }
+ if ( currentMonth == 0 ) {
+ resetMonth();
+ if ( shiftYear() ) {
+ completeTimer();
+ return false;
+ }
+ else {
+ return considerWeekdays();
+ }
+ }
+ if ( currentMonth > current.month.integer ) {
+ resetDays();
+ return considerWeekdays();
+ }
+
+ // day
+ currentDay = 0;
+ dayIndex = expression.daysOfMonth.iterator();
+ while ( is Integer item = dayIndex.next() ) {
+ if ( item >= current.day ) {
+ currentDay = item;
+ break;
+ }
+ }
+ if ( currentDay == 0 ) {
+ resetDays();
+ if ( shiftMonth() ) {
+ completeTimer();
+ return false;
+ }
+ else {
+ return considerWeekdays();
+ }
+ }
+ if ( currentDay > current.day ) {
+ resetHours();
+ return considerWeekdays();
+ }
+
+ // hour
+ currentHour = -1;
+ hourIndex = expression.hours.iterator();
+ while ( is Integer item = hourIndex.next() ) {
+ if ( item >= current.hours ) {
+ currentHour = item;
+ break;
+ }
+ }
+ if ( currentHour == -1 ) {
+ resetHours();
+ if ( shiftDay() ) {
+ completeTimer();
+ return false;
+ }
+ else {
+ return considerWeekdays();
+ }
+ }
+ if ( currentHour > current.hours ) {
+ resetMinutes();
+ return considerWeekdays();
+ }
+
+ // minutes
+ currentMinute = -1;
+ minuteIndex = expression.minutes.iterator();
+ while ( is Integer item = minuteIndex.next() ) {
+ if ( item >= current.minutes ) {
+ currentMinute = item;
+ break;
+ }
+ }
+ if ( currentMinute == -1 ) {
+ resetMinutes();
+ if ( shiftHour() ) {
+ completeTimer();
+ return false;
+ }
+ else {
+ return considerWeekdays();
+ }
+ }
+ if ( currentMinute > current.minutes ) {
+ resetSeconds();
+ return considerWeekdays();
+ }
+
+ // seconds
+ currentSecond = -1;
+ secondIndex = expression.seconds.iterator();
+ while ( is Integer item = secondIndex.next() ) {
+ if ( item >= current.seconds ) {
+ currentSecond = item;
+ break;
+ }
+ }
+ if ( currentSecond == -1 ) {
+ resetSeconds();
+ if ( shiftMinute() ) {
+ completeTimer();
+ return false;
+ }
+ }
+
+ // shift to appropriate day of week
+ return considerWeekdays();
+
+ }
+
+ "Calculates next local time and stores it in [[currentDate]].
+ Returns `true` if successfully shifted and `false` if to be completed."
+ Boolean shiftCronTime() {
+ if ( shiftSecond() ) {
+ completeTimer();
+ return false;
+ }
+ else {
+ return considerWeekdays();
+ }
+ }
+
+
+ /* Timer interface */
+
+ shared actual DateTime? start( DateTime current ) {
+ if ( startCron( current ) ) {
+ return currentDate;
+ }
+ else {
+ return null;
+ }
+ }
+
+ shared actual DateTime? shiftTime() {
+ if ( shiftCronTime() ) {
+ return currentDate;
+ }
+ else {
+ return null;
+ }
+ }
+
+}
diff --git a/source/herd/schedule/chime/timer/TimerFactory.ceylon b/source/herd/schedule/chime/timer/TimerFactory.ceylon
new file mode 100644
index 0000000..d5f0259
--- /dev/null
+++ b/source/herd/schedule/chime/timer/TimerFactory.ceylon
@@ -0,0 +1,55 @@
+import ceylon.json {
+
+ JSON=Object
+}
+import ceylon.collection {
+
+ HashMap
+}
+import herd.schedule.chime {
+
+ errorMessages
+}
+
+
+"Factory to create timers."
+by( "Lis" )
+shared interface TimerFactory
+{
+ shared formal Timer|String createTimer( "Timer description." JSON description );
+}
+
+
+"Base timer factory.
+ Uses type -> creator function map to create timers.
+ Before create timers add creators using [[addCreator]]"
+by( "Lis" )
+shared class FactoryJSONBase() satisfies TimerFactory
+{
+
+ "type -> creator function map"
+ HashMap(JSON)> creators = HashMap(JSON)>();
+
+
+ "Adds creator to the factory."
+ shared void addCreator( "Timer type." String type, "Creator function." (JSON) creator ) {
+ creators.put( type, creator );
+ }
+
+ "Searches creators from added via [[addCreator]] and use them to create timers.
+ description to contain field \"type\" which is used to find creator function."
+ shared actual Timer|String createTimer( "timer description" JSON description ) {
+ if ( is String type = description.get( definitions.type ) ) {
+ if ( exists creator = creators.get( type ) ) {
+ return creator( description );
+ }
+ else {
+ return errorMessages.unsupportedTimerType;
+ }
+ }
+ else {
+ return errorMessages.timerTypeHasToBeSpecified;
+ }
+ }
+
+}
diff --git a/source/herd/schedule/chime/timer/TimerInterval.ceylon b/source/herd/schedule/chime/timer/TimerInterval.ceylon
new file mode 100644
index 0000000..1f6c98b
--- /dev/null
+++ b/source/herd/schedule/chime/timer/TimerInterval.ceylon
@@ -0,0 +1,35 @@
+import ceylon.time {
+
+ DateTime,
+ dateTime
+}
+
+
+"Incremental timer - starts from specific time date
+ and increments on certain miliseconds each time when fired - [[intervalMilliseconds]].
+ [[intervalMilliseconds]] to be >= 0.
+ "
+by( "Lis" )
+class TimerInterval (
+ "Timing interval in miliseconds, to be >= 0." shared Integer intervalMilliseconds
+)
+ satisfies Timer
+{
+ "Current date and time."
+ variable DateTime currentDate = dateTime( 0, 1, 1 );
+
+
+ /* Timer interface */
+
+ shared actual DateTime? start( DateTime current ) {
+ // next fire time
+ currentDate = current.plusMilliseconds( intervalMilliseconds );
+ return currentDate;
+ }
+
+ shared actual DateTime? shiftTime() {
+ currentDate = currentDate.plusMilliseconds( intervalMilliseconds );
+ return currentDate;
+ }
+
+}
diff --git a/source/herd/schedule/chime/timer/definitions.ceylon b/source/herd/schedule/chime/timer/definitions.ceylon
new file mode 100644
index 0000000..acd5622
--- /dev/null
+++ b/source/herd/schedule/chime/timer/definitions.ceylon
@@ -0,0 +1,104 @@
+
+
+"Defines scheduler constants."
+by( "Lis" )
+shared object definitions
+{
+
+ // configuration
+
+ "default listening address"
+ shared String defaultAddress = "chime";
+
+
+ // configuration names
+
+ "listening address"
+ shared String address = "address";
+
+ "max year period limit"
+ shared String maxYearPeriodLimit = "max year period limit";
+
+ "tolerance to compare fire time and current time in miliseconds"
+ shared String tolerance = "tolerance";
+
+
+ "separator of manager and timer name"
+ shared String nameSeparator = ":";
+
+
+
+ // time scheduler command / return fields
+
+ "operation - to contain operation code"
+ shared String fieldOperation = "operation";
+
+ "timer name"
+ shared String fieldName = "name";
+
+ "timer state"
+ shared String fieldState = "state";
+
+ "response code"
+ shared String fieldResponse = "response";
+
+ "error field - contains error description, fieldResponse to be `error`"
+ shared String fieldError = "error";
+
+ "timer description"
+ shared String fieldDescription = "descirption";
+
+ "time field"
+ shared String fieldTime = "time";
+
+ "time count"
+ shared String fieldCount = "count";
+
+ "field with schedulers array"
+ shared String fieldSchedulers = "schedulers";
+
+ "field with timers array"
+ shared String fieldTimers = "timers";
+
+
+ "field max count"
+ shared String fieldMaxCount = "max count";
+
+ "publish field"
+ shared String fieldPublish = "publish";
+
+ "start time field"
+ shared String fieldStartTime = "start time";
+ "end time field"
+ shared String fieldEndTime = "end time";
+
+
+ // time zone
+ shared String timeZoneID = "time zone";
+
+
+ // operation codes
+
+ "create timer"
+ shared String opCreate = "create";
+ "delete timer"
+ shared String opDelete = "delete";
+ "get or modify shceduler or timer state (pause, run)"
+ shared String opState = "state";
+ "get total or scheduler info"
+ shared String opInfo = "info";
+
+ "get state"
+ shared String stateGet = "get";
+
+ "timer description type field"
+ shared String type = "type";
+
+ // response codes
+
+ "operation accepted"
+ shared String responseOK = "ok";
+ "error has been occured during operation execution - see `error` field"
+ shared String responseError = "error";
+
+}
diff --git a/source/herd/schedule/chime/timer/package.ceylon b/source/herd/schedule/chime/timer/package.ceylon
new file mode 100644
index 0000000..2407ab8
--- /dev/null
+++ b/source/herd/schedule/chime/timer/package.ceylon
@@ -0,0 +1,7 @@
+"Contains different timers and factory to create timers.
+ Supported timers:
+ * cron-style timer [[TimerCronStyle]]
+ * interval timer [[TimerInterval]]
+ "
+by( "Lis" )
+package herd.schedule.chime.timer;
diff --git a/source/herd/schedule/chime/timer/timerDefinitions.ceylon b/source/herd/schedule/chime/timer/timerDefinitions.ceylon
new file mode 100644
index 0000000..fdb541b
--- /dev/null
+++ b/source/herd/schedule/chime/timer/timerDefinitions.ceylon
@@ -0,0 +1,15 @@
+
+
+"defines timer constants"
+by( "Lis" )
+object timerDefinitions
+{
+
+ // increment in seconds
+ shared String intervalSeconds = "interval seconds";
+
+ // timer types
+ shared String typeCronStyle = "cron";
+ shared String typeInterval = "interval";
+
+}