├── .ceylon
├── config
└── config~
├── .classpath
├── .gitignore
├── .project
├── .settings
└── .gitignore
├── LICENSE
├── README.md
├── examples
└── herd
│ └── examples
│ └── schedule
│ └── chime
│ ├── customtimer
│ ├── customTimer.ceylon
│ ├── package.ceylon
│ └── run.ceylon
│ ├── module.ceylon
│ ├── package.ceylon
│ └── run.ceylon
├── howto.md
├── scheduling.md
├── source
└── herd
│ └── schedule
│ └── chime
│ ├── CalendarService.ceylon
│ ├── Chime.ceylon
│ ├── ChimeServiceProvider.ceylon
│ ├── CronBuilder.ceylon
│ ├── Operator.ceylon
│ ├── Scheduler.ceylon
│ ├── SchedulerImpl.ceylon
│ ├── SchedulerInfo.ceylon
│ ├── SchedulerManager.ceylon
│ ├── Sender.ceylon
│ ├── State.ceylon
│ ├── TimeScheduler.ceylon
│ ├── TimeServices.ceylon
│ ├── TimeUnit.ceylon
│ ├── Timer.ceylon
│ ├── TimerContainer.ceylon
│ ├── TimerCreator.ceylon
│ ├── TimerEvent.ceylon
│ ├── TimerImpl.ceylon
│ ├── TimerInfo.ceylon
│ ├── UnionBuilder.ceylon
│ ├── calendarBuilder.ceylon
│ ├── cron
│ ├── CronExpression.ceylon
│ ├── DaysOrder.ceylon
│ ├── calendar.ceylon
│ ├── cronDefinitions.ceylon
│ ├── package.ceylon
│ ├── parseCron.ceylon
│ ├── parseCronDaysOfWeek.ceylon
│ ├── parseCronRange.ceylon
│ ├── parseCronStyle.ceylon
│ └── stringToInteger.ceylon
│ ├── dateTimeFromJSON.ceylon
│ ├── every.ceylon
│ ├── extractors.ceylon
│ ├── module.ceylon
│ ├── package.ceylon
│ ├── schedulerTopLevel.ceylon
│ ├── service
│ ├── ChimeServices.ceylon
│ ├── Extension.ceylon
│ ├── calendar
│ │ ├── Annually.ceylon
│ │ ├── Calendar.ceylon
│ │ ├── CalendarFactory.ceylon
│ │ ├── Daily.ceylon
│ │ ├── Dativity.ceylon
│ │ ├── DayMonth.ceylon
│ │ ├── IntersectionCalendar.ceylon
│ │ ├── LastDayOfWeek.ceylon
│ │ ├── Monthly.ceylon
│ │ ├── UnionCalendar.ceylon
│ │ ├── WeekdayOfMonth.ceylon
│ │ ├── Weekly.ceylon
│ │ ├── extractCalendars.ceylon
│ │ └── package.ceylon
│ ├── message
│ │ ├── DirectMessageSourceFactory.ceylon
│ │ ├── MessageSource.ceylon
│ │ ├── MessageSourceFactory.ceylon
│ │ └── package.ceylon
│ ├── package.ceylon
│ ├── producer
│ │ ├── EBProducerFactory.ceylon
│ │ ├── EventBusProducer.ceylon
│ │ ├── EventProducer.ceylon
│ │ ├── ProducerFactory.ceylon
│ │ └── package.ceylon
│ ├── timer
│ │ ├── CronFactory.ceylon
│ │ ├── IntervalFactory.ceylon
│ │ ├── TimeRow.ceylon
│ │ ├── TimeRowCronStyle.ceylon
│ │ ├── TimeRowFactory.ceylon
│ │ ├── TimeRowInterval.ceylon
│ │ ├── TimeRowUnion.ceylon
│ │ ├── UnionFactory.ceylon
│ │ ├── emptyTimeRow.ceylon
│ │ └── package.ceylon
│ └── timezone
│ │ ├── JVMTimeZone.ceylon
│ │ ├── JVMTimeZoneFactory.ceylon
│ │ ├── TimeZone.ceylon
│ │ ├── TimeZoneFactory.ceylon
│ │ └── package.ceylon
│ └── timerTopLevel.ceylon
└── test
└── herd
└── test
└── schedule
└── chime
├── CronBuilder.ceylon
├── SchedulerTimer.ceylon
├── SimpleTimers.ceylon
├── module.ceylon
└── package.ceylon
/.ceylon/config:
--------------------------------------------------------------------------------
1 |
2 | [compiler]
3 | source=source
4 | resource=resource
5 | source=test
6 | source=examples
7 |
8 | [defaults]
9 | encoding=UTF-8
10 |
--------------------------------------------------------------------------------
/.ceylon/config~:
--------------------------------------------------------------------------------
1 |
2 | [compiler]
3 | source=source
4 | resource=resource
5 | source=examples
6 |
7 | [defaults]
8 | encoding=UTF-8
9 |
--------------------------------------------------------------------------------
/.classpath:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.class
2 |
3 | # Mobile Tools for Java (J2ME)
4 | .mtj.tmp/
5 |
6 | # Package Files #
7 | *.jar
8 | *.war
9 | *.ear
10 |
11 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
12 | hs_err_pid*
13 | /.exploded/
14 | /modules
15 | /classes/
16 |
--------------------------------------------------------------------------------
/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | Chime
4 |
5 |
6 |
7 |
8 |
9 | org.eclipse.jdt.core.javabuilder
10 |
11 |
12 |
13 |
14 | com.redhat.ceylon.eclipse.ui.ceylonBuilder
15 |
16 |
17 | systemRepo
18 |
19 |
20 |
21 |
22 |
23 |
24 | com.redhat.ceylon.eclipse.ui.ceylonNature
25 | org.eclipse.jdt.core.javanature
26 |
27 |
28 |
--------------------------------------------------------------------------------
/.settings/.gitignore:
--------------------------------------------------------------------------------
1 | /org.eclipse.jdt.core.prefs
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Lisi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Chime.
2 |
3 | _Chime_ is time scheduler verticle which works on [Vert.x](http://vertx.io/) event bus and provides:
4 | * scheduling with _cron-style_, _interval_, _union_ or _custom_ timers:
5 | * at a certain time of day (to the second)
6 | * on certain days of the week, month or year
7 | * with a given time interval
8 | * with nearly any combination of all of above
9 | * repeating a given number of times
10 | * repeating until a given time / date
11 | * repeating infinitely
12 | * proxying event bus with conventional interfaces
13 | * applying time zones available on _JVM_
14 | * flexible timers management system:
15 | * grouping timers
16 | * defining a timer start or end time
17 | * pausing / resuming
18 | * fire counting
19 | * listening and sending messages via event bus with _JSON_
20 | * _publishing_ or _sending_ timer fire event to the address of your choice
21 |
22 | _Chime_ is written in [Ceylon](https://ceylon-lang.org) and is available at
23 | [Ceylon Herd](https://herd.ceylon-lang.org/modules/herd.schedule.chime)
24 |
25 | > Runs with Ceylon 1.3.2 and Vert.x 3.4.1
26 |
27 |
28 | ## Usage and documentation.
29 |
30 | 1. Deploy _Chime_ verticle
31 | 2. Create and listen timers on _EventBus_, see details in [API docs](https://modules.ceylon-lang.org/repo/1/herd/schedule/chime/0.2.0/module-doc/api/index.html)
32 |
33 | > _Chime_ communicates over event bus with `Json` messages.
34 | Complete list of messages is available [here](../../wiki/Messages)
35 |
36 | Examples:
37 | * [with Ceylon](examples/herd/examples/schedule/chime)
38 | * [with Java and Maven](https://github.com/LisiLisenok/ChimeJavaExample)
39 |
40 |
41 | [Blog post at Vert.x website](http://vertx.io/blog/time-scheduling-with-chime/)
42 |
--------------------------------------------------------------------------------
/examples/herd/examples/schedule/chime/customtimer/customTimer.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.time {
2 | dateTime,
3 | DateTime
4 | }
5 | import herd.schedule.chime.service.timer {
6 | TimeRowFactory,
7 | TimeRow
8 | }
9 | import herd.schedule.chime.service {
10 | ChimeServices,
11 | Extension
12 | }
13 | import ceylon.json {
14 | JsonObject
15 | }
16 | import herd.schedule.chime {
17 | Chime
18 | }
19 |
20 |
21 | // Custom timer factory
22 | service(`interface Extension`)
23 | shared class CustomIntervalTimerFactory satisfies TimeRowFactory
24 | {
25 | shared static String timerType = "custom interval";
26 |
27 | shared new () {}
28 |
29 | // Custom timer
30 | class TimeRowInterval (
31 | "Timing delay in miliseconds, to be >= 0." shared Integer intervalMilliseconds
32 | ) satisfies TimeRow {
33 | "Current date and time."
34 | variable DateTime currentDate = dateTime(0, 1, 1);
35 | shared actual DateTime? start(DateTime current) => currentDate = current.plusMilliseconds(intervalMilliseconds);
36 | shared actual DateTime? shiftTime() => currentDate = currentDate.plusMilliseconds(intervalMilliseconds);
37 | }
38 |
39 | shared actual TimeRow|String> create(ChimeServices services, JsonObject description) {
40 | if (is Integer delay = description[Chime.key.delay]) {
41 | if (delay > 0) {
42 | return TimeRowInterval(delay * 1000);
43 | }
44 | else {
45 | return Chime.errors.codeDelayHasToBeGreaterThanZero->Chime.errors.delayHasToBeGreaterThanZero;
46 | }
47 | }
48 | return Chime.errors.codeDelayHasToBeSpecified->Chime.errors.delayHasToBeSpecified;
49 | }
50 |
51 | shared actual String type => timerType;
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/examples/herd/examples/schedule/chime/customtimer/package.ceylon:
--------------------------------------------------------------------------------
1 | "Custom timer example."
2 | since( "0.1.1" ) by( "Lis" )
3 | shared package herd.examples.schedule.chime.customtimer;
4 |
--------------------------------------------------------------------------------
/examples/herd/examples/schedule/chime/customtimer/run.ceylon:
--------------------------------------------------------------------------------
1 | import io.vertx.ceylon.core.eventbus {
2 | Message
3 | }
4 | import ceylon.json {
5 | JsonObject,
6 | JsonArray
7 | }
8 | import herd.schedule.chime {
9 | Chime
10 | }
11 | import io.vertx.ceylon.core {
12 | DeploymentOptions,
13 | vertx
14 | }
15 |
16 |
17 | shared void run() {
18 | String timerName = "scheduler:timer";
19 | value v = vertx.vertx();
20 | Chime c = Chime();
21 | // deploy Chime using config with search service providers module list
22 | c.deploy (
23 | v,
24 | DeploymentOptions (
25 | JsonObject {
26 | // module to search custom timer
27 | Chime.configuration.services -> JsonArray{"herd.examples.schedule.chime/0.3.0"}
28 | }
29 | ),
30 | (String|Throwable res) {
31 | if (is String res) {
32 | // print installed extension
33 | v.eventBus().send (
34 | "chime",
35 | JsonObject {
36 | Chime.key.operation -> Chime.operation.info
37 | },
38 | (Throwable|Message msg) {
39 | if (is Message msg, exists body = msg.body()) {
40 | if (exists services = body.getArrayOrNull(Chime.configuration.services)) {
41 | for (item in services) {
42 | print(item);
43 | }
44 | }
45 | else {
46 | print("extensions are not given");
47 | }
48 | }
49 | else {
50 | print(msg);
51 | }
52 | }
53 | );
54 | // listen timer messages
55 | v.eventBus().consumer (
56 | timerName,
57 | (Throwable|Message msg) {
58 | if (is Message msg, exists body = msg.body()) {
59 | if (is String event = body[Chime.key.event]) {
60 | if (event == Chime.event.complete) {
61 | print("completed");
62 | v.close();
63 | }
64 | else if (event == Chime.event.fire) {
65 | print("fire at ``body[Chime.key.time] else ""`` with ``body[Chime.key.message] else ""``");
66 | }
67 | else {
68 | print("undefined event: ``event``");
69 | v.close();
70 | }
71 | }
72 | else {
73 | print("no event in ``msg``");
74 | v.close();
75 | }
76 | }
77 | else {
78 | print(msg);
79 | v.close();
80 | }
81 | }
82 | );
83 | // create custom timer
84 | v.eventBus().send (
85 | "chime",
86 | JsonObject {
87 | Chime.key.operation -> Chime.operation.create,
88 | Chime.key.name -> timerName,
89 | Chime.key.maxCount -> 3,
90 | Chime.key.description -> JsonObject {
91 | // the same type as factory marked with
92 | Chime.key.type -> CustomIntervalTimerFactory.timerType,
93 | Chime.key.delay -> 1
94 | },
95 | Chime.key.message -> "timer message"
96 | }
97 | );
98 | }
99 | else {
100 | print("deploying error: ``res``");
101 | v.close();
102 | }
103 | }
104 | );
105 |
106 | }
107 |
--------------------------------------------------------------------------------
/examples/herd/examples/schedule/chime/module.ceylon:
--------------------------------------------------------------------------------
1 |
2 | "_Chime_ examples."
3 | since("0.1.1") by("Lis")
4 | native("jvm")
5 | module herd.examples.schedule.chime "0.3.0" {
6 | shared import io.vertx.ceylon.core "3.4.2";
7 | shared import herd.schedule.chime "0.3.0";
8 | }
9 |
--------------------------------------------------------------------------------
/examples/herd/examples/schedule/chime/package.ceylon:
--------------------------------------------------------------------------------
1 | "Simple example of cron timer."
2 | since( "0.1.1" ) by( "Lis" )
3 | shared package herd.examples.schedule.chime;
4 |
--------------------------------------------------------------------------------
/examples/herd/examples/schedule/chime/run.ceylon:
--------------------------------------------------------------------------------
1 | import io.vertx.ceylon.core.eventbus {
2 |
3 | EventBus,
4 | Message
5 | }
6 | import ceylon.json {
7 |
8 | JsonObject
9 | }
10 | import io.vertx.ceylon.core {
11 |
12 | Vertx,
13 | vertx
14 | }
15 | import herd.schedule.chime {
16 | Chime
17 | }
18 |
19 |
20 | "Runs the module `herd.examples.schedule.chime`."
21 | shared void run() {
22 | value v = vertx.vertx();
23 | Chime c = Chime();
24 | c.deploy (
25 | v, null,
26 | (String|Throwable res) {
27 | if (is String res) {
28 | value scheduler = Scheduler(v);
29 | scheduler.initialize();
30 | }
31 | else {
32 | print("deploying error: ``res``");
33 | v.close();
34 | }
35 | }
36 | );
37 | }
38 |
39 |
40 | "Performs scheduler run. Creates cron-style timer and listens it."
41 | class Scheduler(Vertx v, String address = Chime.configuration.defaultAddress)
42 | {
43 | EventBus eventBus = v.eventBus();
44 |
45 |
46 | "Initializes testing - creates schedule manager and timer."
47 | shared void initialize() {
48 | eventBus.send (
49 | address,
50 | JsonObject {
51 | Chime.key.operation -> Chime.operation.create,
52 | Chime.key.name -> "scheduler",
53 | Chime.key.state -> Chime.state.running,
54 | Chime.key.timeZone -> "Europe/Paris"
55 | },
56 | (Throwable | Message msg) {
57 | if (is Message msg) {
58 | schedulerCreated(msg);
59 | }
60 | else {
61 | print("error when creating scheduler: ``msg``");
62 | v.close();
63 | }
64 | }
65 | );
66 | }
67 |
68 |
69 | void printMessage(Throwable|Message msg) {
70 | if (is Message msg) {
71 | if (exists body = msg.body()) {
72 | print(body);
73 | if (is String event = body[Chime.key.event], event == Chime.event.complete) {
74 | v.close();
75 | }
76 | }
77 | else {
78 | print("no body in the message");
79 | v.close();
80 | }
81 | }
82 | else {
83 | print("error: ``msg``");
84 | v.close();
85 | }
86 | }
87 |
88 |
89 | void schedulerCreated(Message msg) {
90 |
91 | eventBus.consumer("scheduler:timer", printMessage);
92 |
93 | eventBus.send (
94 | address,
95 | JsonObject {
96 | Chime.key.operation -> Chime.operation.create,
97 | Chime.key.name -> "scheduler:timer",
98 | Chime.key.maxCount -> 5,
99 | //Chime.key.timeZone -> "Europe/Paris",
100 | Chime.key.description -> JsonObject {
101 | Chime.key.type -> Chime.type.cron,
102 | Chime.date.seconds -> "20/15",
103 | Chime.date.minutes -> "*",
104 | Chime.date.hours -> "0-23",
105 | Chime.date.daysOfMonth -> "1-31",
106 | Chime.date.months -> "*",
107 | Chime.date.daysOfWeek -> "*",
108 | Chime.date.years -> "2015-2019"
109 | }
110 | },
111 | printMessage
112 | );
113 | }
114 |
115 | }
--------------------------------------------------------------------------------
/scheduling.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Time scheduling with Chime
3 | template: post.html
4 | date: 2017-05-09
5 | author: LisiLisenok
6 | draft: true
7 | ---
8 |
9 | ## Time scheduling.
10 |
11 | Vert.x executes periodic and delayed actions with
12 | [one-shot and periodic timers](http://vertx.io/docs/vertx-core/java/#_executing_periodic_and_delayed_actions).
13 | This is the base for time scheduling and reach feature extension must be rather interesting.
14 | Be notified at certain date / time, take into account holidays,
15 | repeat notifications until a given date, apply time zone,
16 | take into account daylight saving time etc.
17 | There are a lot of useful features time scheduler may introduce to the Vert.x stack.
18 |
19 |
20 | ## Chime.
21 |
22 | [Chime](https://github.com/LisiLisenok/Chime) is time scheduler verticle which works on _Vert.x_ event bus and provides:
23 | * scheduling with _cron-style_, _interval_ or _union_ timers:
24 | * at a certain time of day (to the second);
25 | * on certain days of the week, month or year;
26 | * with a given time interval;
27 | * with nearly any combination of all of above;
28 | * repeating a given number of times;
29 | * repeating until a given time / date;
30 | * repeating infinitely
31 | * proxying event bus with conventional interfaces
32 | * applying time zones available on _JVM_
33 | * flexible timers management system:
34 | * grouping timers;
35 | * defining a timer start or end times
36 | * pausing / resuming;
37 | * fire counting;
38 | * listening and sending messages via event bus with _JSON_;
39 | * _publishing_ or _sending_ timer fire event to the address of your choice.
40 |
41 | [INFO _Chime_ is written in [Ceylon](https://ceylon-lang.org) and is available at
42 | [Ceylon Herd](https://herd.ceylon-lang.org/modules/herd.schedule.chime).]
43 |
44 |
45 | ## Running.
46 |
47 | ### Ceylon users.
48 |
49 | Deploy _Chime_ using `Verticle.deployVerticle` method.
50 |
51 | ```Ceylon
52 | import io.vertx.ceylon.core {vertx}
53 | import herd.schedule.chime {Chime}
54 | Chime().deploy(vertx.vertx());
55 | ```
56 |
57 | Or with `vertx.deployVerticle(\"ceylon:herd.schedule.chime/0.2.1\");`
58 | but ensure that Ceylon verticle factory is available at class path.
59 |
60 | ### Java users.
61 |
62 | 1. Ensure that Ceylon verticle factory is available at class path.
63 | 2. Put Ceylon versions to consistency. For instance, Vert.x 3.4.1 depends on Ceylon 1.3.0
64 | while Chime 0.2.1 depends on Ceylon 1.3.2.
65 | 3. [Deploy verticle](http://vertx.io/docs/vertx-core/java/#_deploying_verticles_programmatically), like:
66 | ```Java
67 | vertx.deployVerticle("ceylon:herd.schedule.chime/0.2.1")
68 | ```
69 |
70 | [INFO example with Maven is available at [Github](https://github.com/LisiLisenok/ChimeJavaExample).]
71 |
72 |
73 | ## Schedulers.
74 |
75 | Well, _Chime_ verticle is deployed. Let's see its structure.
76 | In order to provide flexible and broad ways to manage timing two level architecture is adopted.
77 | It consists of schedulers and timers. Timer is a unit which fires at a given time.
78 | While scheduler is a set or group of timers and provides following:
79 | * creating and deleting timers;
80 | * pausing / resuming all timers working within the scheduler;
81 | * info on the running timers;
82 | * default time zone;
83 | * listening event bus at the given scheduler address for the requests to.
84 |
85 | Any timer operates within some scheduler. And one or several schedulers have to be created before starting scheduling.
86 | When _Chime_ verticle is deployed it starts listen event bus at **chime** address (can be configured).
87 | In order to create scheduler send to this address a JSON message.
88 |
89 | ```json
90 | {
91 | "operation": "create",
92 | "name": "scheduler name"
93 | }
94 | ```
95 |
96 |
97 | Once scheduler is created it starts listen event bus at **scheduler name** address.
98 | Sending messages to **chime** address or to **scheduler name** address are rather equivalent,
99 | excepting that chime address provides services for every scheduler, while scheduler address
100 | provides services for this particular scheduler only.
101 | The request sent to the _Chime_ has to contain **operation** and **name** keys.
102 | Name key provides scheduler or timer name. While operation key shows an action _Chime_ has to perform.
103 | There are only four possible operations:
104 | * create - create new scheduler or timer;
105 | * delete - delete scheduler or timer;
106 | * info - request info on _Chime_ or on a particular scheduler or timer;
107 | * state - set or get scheduler or timer state (running, paused or completed).
108 |
109 |
110 | ## Timers.
111 |
112 | Now we have scheduler created and timers can be run within. There are two ways to access a given timer:
113 | 1. Sending message to **chime** address with 'name' field set to **scheduler name:timer name**.
114 | 2. Sending message to **scheduler name** address with 'name' field set to either **timer name** or **scheduler name:timer name**.
115 |
116 | [Timer request](https://modules.ceylon-lang.org/repo/1/herd/schedule/chime/0.2.1/module-doc/api/index.html#timer-request) is rather complicated and contains a lot of details. In this blog post only basic features are considered:
117 |
118 | ```json
119 | {
120 | "operation": "create",
121 | "name": "scheduler name:timer name",
122 | "description": {}
123 | };
124 | ```
125 |
126 | This is rather similar to request sent to create a scheduler.
127 | The difference is only **description** field is added.
128 | This description is an JSON object which identifies particular timer type and its details.
129 | The other fields not shown here are optional and includes:
130 | * initial timer state (paused or running);
131 | * start or end date-time;
132 | * number of repeating times;
133 | * is timer message to be published or sent;
134 | * timer fire message and delivery options;
135 | * time zone.
136 |
137 |
138 | ## Timer descriptions.
139 |
140 | Currently, three types of timers are supported:
141 |
142 | * __Interval timer__ which fires after each given time period (minimum 1 second):
143 | ```json
144 | {
145 | "type": "interval",
146 | "delay": "timer delay in seconds, Integer"
147 | };
148 | ```
149 |
150 | * __Cron style timer__ which is defined with cron-style:
151 | ```json
152 | {
153 | "type": "cron",
154 | "seconds": "seconds in cron style, String",
155 | "minutes": "minutes in cron style, String",
156 | "hours": "hours in cron style, String",
157 | "days of month": "days of month in cron style, String",
158 | "months": "months in cron style, String",
159 | "days of week": "days of week in cron style, String, optional",
160 | "years": "years in cron style, String, optional"
161 | };
162 | ```
163 | Cron timer is rather powerful and flexible. Investigate [specification](https://modules.ceylon-lang.org/repo/1/herd/schedule/chime/0.2.1/module-doc/api/index.html#cron-expression) for the complete list of features.
164 |
165 | * __Union timer__ which combines a number of timers into a one:
166 | ```json
167 | {
168 | "type": "union",
169 | "timers": ["list of the timer descriptions"]
170 | };
171 | ```
172 | Union timer may be useful to fire at a list of specific dates / times.
173 |
174 |
175 | ## Timer events.
176 |
177 | Once timer is started it sends or publishes messages to **scheduler name:timer name** address in JSON format.
178 | Two types of events are sent:
179 | * fire event which occurs when time reaches next timer value:
180 | ```json
181 | {
182 | "name": "scheduler name:timer name, String",
183 | "event": "fire",
184 | "count": "total number of fire times, Integer",
185 | "time": "ISO formated time / date, String",
186 | "seconds": "number of seconds since last minute, Integer",
187 | "minutes": "number of minutes since last hour, Integer",
188 | "hours": "hour of day, Integer",
189 | "day of month": "day of month, Integer",
190 | "month": "month, Integer",
191 | "year": "year, Integer",
192 | "time zone": "time zone the timer works in, String"
193 | };
194 | ```
195 | * complete event which occurs when timer is exhausted by some criteria given at timer create request:
196 | ```json
197 | {
198 | "name": "scheduler name:timer name, String",
199 | "event": "complete",
200 | "count": "total number of fire times, Integer"
201 | };
202 | ```
203 |
204 | Basically, now we know everything to be happy with _Chime_: schedulers and requests to them, timers and timer events.
205 | Will see some examples in the next section.
206 |
207 |
208 | ## Examples.
209 |
210 | ### Ceylon example.
211 |
212 | Let's consider a timer which has to fire every month at 16-30 last Sunday.
213 |
214 | ```Ceylon
215 | // listen the timer events
216 | eventBus.consumer (
217 | "my scheduler:my timer",
218 | (Throwable|Message msg) {
219 | if (is Message msg) { print(msg.body()); }
220 | else { print(msg); }
221 | }
222 | );
223 | // create scheduler and timer
224 | eventBus.send (
225 | "chime",
226 | JsonObject {
227 | "operation" -> "create",
228 | "name" -> "my scheduler:my timer",
229 | "description" -> JsonObject {
230 | "type" -> "cron",
231 | "seconds" -> "0",
232 | "minutes" -> "30",
233 | "hours" -> "16",
234 | "days of month" -> "*",
235 | "months" -> "*",
236 | "days of week" -> "SundayL"
237 | }
238 | }
239 | );
240 |
241 | ```
242 |
243 | [INFO '*' means any, 'SundayL' means last Sunday.]
244 |
245 | [NOTE If 'create' request is sent to Chime address with name set to 'scheduler name:timer name' and corresponding scheduler hasn't been created before then Chime creates both new scheduler and new timer.]
246 |
247 |
248 | ### Java example.
249 |
250 | Let's consider a timer which has to fire every Monday at 8-30 and every Friday at 17-30.
251 |
252 | ```Java
253 | // listen the timer events
254 | MessageConsumer consumer = eventBus.consumer("my scheduler:my timer");
255 | consumer.handler (
256 | message -> {
257 | System.out.println(message.body());
258 | }
259 | );
260 | // description of timers
261 | JsonObject mondayTimer = (new JsonObject()).put("type", "cron")
262 | .put("seconds", "0").put("minutes", "30").put("hours", "8")
263 | .put("days of month", "*").put("months", "*")
264 | .put("days of week", "Monday");
265 | JsonObject fridayTimer = (new JsonObject()).put("type", "cron")
266 | .put("seconds", "0").put("minutes", "30").put("hours", "17")
267 | .put("days of month", "*").put("months", "*")
268 | .put("days of week", "Friday");
269 | // union timer - combines mondayTimer and fridayTimer
270 | JsonArray combination = (new JsonArray()).add(mondayTimer)
271 | .add(fridayTimer);
272 | JsonObject timer = (new JsonObject()).put("type", "union")
273 | .put("timers", combination);
274 | // create scheduler and timer
275 | eventBus.send (
276 | "chime",
277 | (new JsonObject()).put("operation", "create")
278 | .put("name", "my scheduler:my timer")
279 | .put("description", timer)
280 | );
281 | ```
282 |
283 | [IMPORTANT Ensure that Ceylon verticle factory with right version is available at class path.]
284 |
285 |
286 | ## At the end.
287 |
288 | `herd.schedule.chime` module provides some features not mentioned here:
289 | * convenient builders useful to fill in JSON description of various timers;
290 | * proxying event bus with conventional interfaces;
291 | * reading JSON timer event into an object;
292 | * attaching JSON message to the timer fire event;
293 | * managing time zones.
294 |
295 | There are also some ideas for the future:
296 | * custom or user-defined timers;
297 | * limiting the timer fire time / date with calendar;
298 | * extracting timer fire message from external source.
299 |
300 | -----------------------------
301 |
302 | This is very quick introduction to the _Chime_ and if you are interested in you may read
303 | more in [Chime documentation](https://modules.ceylon-lang.org/repo/1/herd/schedule/chime/0.2.1/module-doc/api/index.html) or even [contribute](https://github.com/LisiLisenok/Chime) to.
304 |
305 | Thank's for the reading and enjoy with coding!
306 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/CalendarService.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.time {
2 | DateTime
3 | }
4 | import herd.schedule.chime.service.calendar {
5 | Calendar
6 | }
7 |
8 |
9 | "Calendar + ignorance flage."
10 | since("0.3.0") by("Lis")
11 | interface CalendarService satisfies Calendar {
12 | "Calendar ignorance flag." shared formal Boolean calendarIgnorance;
13 | }
14 |
15 |
16 | "Implementation of `CalendarService`."
17 | since("0.3.0") by("Lis")
18 | class CalendarServiceImpl (
19 | shared actual Boolean calendarIgnorance,
20 | Calendar calendar
21 | )
22 | satisfies CalendarService
23 | {
24 | shared actual Boolean inside(DateTime date) => calendar.inside(date);
25 |
26 | shared actual DateTime nextOutside(DateTime date) => calendar.nextOutside(date);
27 | }
28 |
29 |
30 | "[[CalendarService]] which restricts nothing."
31 | since("0.3.0") by("Lis")
32 | object emptyCalendar satisfies CalendarService {
33 | shared actual Boolean inside(DateTime date) => false;
34 | shared actual DateTime nextOutside(DateTime date) => date.plusSeconds(1);
35 | shared actual Boolean calendarIgnorance => true;
36 | }
37 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/ChimeServiceProvider.ceylon:
--------------------------------------------------------------------------------
1 | import herd.schedule.chime.service {
2 | ChimeServices,
3 | Extension
4 | }
5 | import herd.schedule.chime.service.timezone {
6 | TimeZone,
7 | JVMTimeZoneFactory
8 | }
9 | import ceylon.collection {
10 | HashMap,
11 | ArrayList
12 | }
13 | import ceylon.json {
14 | JsonObject,
15 | JsonArray
16 | }
17 | import ceylon.language.meta {
18 | modules,
19 | type
20 | }
21 | import ceylon.language.meta.declaration {
22 | Module
23 | }
24 | import ceylon.language.meta.model {
25 | ClassOrInterface,
26 | Type
27 | }
28 | import io.vertx.ceylon.core {
29 | Vertx,
30 | Future,
31 | future
32 | }
33 |
34 |
35 | "Provides Chime services."
36 | since("0.3.0") by("Lis")
37 | class ChimeServiceProvider satisfies ChimeServices
38 | {
39 |
40 | static String[2]? moduleNameAndVersion(String fullName) {
41 | value r = fullName.split('/'.equals);
42 | if (r.size == 2) {
43 | return [r.first.normalized, r.last.normalized];
44 | }
45 | else {
46 | return null;
47 | }
48 | }
49 |
50 |
51 | "Collects service providers from the given module."
52 | static void collectFromModule (
53 | "Module to extract service provider from." Module m,
54 | "Interface represented service." ClassOrInterface> providerType,
55 | "Map to collect providers. `key` is given with [[Extension.type]]." ArrayList> extensions
56 | ) {
57 | value services = m.findServiceProviders>(providerType);
58 | for (serv in services) {
59 | extensions.add(serv);
60 | }
61 | }
62 |
63 | "Collects time row service providers."
64 | static {Extension*} collectServices (
65 | "module name + version" {String*} moduleNames,
66 | "Service type." ClassOrInterface> providerType
67 | ) {
68 | ArrayList> extensions = ArrayList>();
69 | for (moduleName in moduleNames) {
70 | if (exists splitName = moduleNameAndVersion(moduleName),
71 | exists m = modules.find(splitName[0], splitName[1])
72 | ) {
73 | collectFromModule(m, providerType, extensions);
74 | }
75 | else {
76 | // TODO: log the module hasn't been found
77 | }
78 | }
79 | if (extensions.empty) {
80 | // If nothing has been added - add extensions from this modue only,
81 | // otherwise extensions from this module have to be already added
82 | collectFromModule(`module`, providerType, extensions);
83 | }
84 | return extensions;
85 | }
86 |
87 |
88 | "Factories to create extension."
89 | HashMap, HashMap>> providers
90 | = HashMap, HashMap>>();
91 |
92 | shared actual Vertx vertx;
93 | shared actual String address;
94 |
95 |
96 | "Instantiates new Chime service provider."
97 | shared new (
98 | "Vertx the Chime is run within." Vertx vertx,
99 | "Event bus address the _Chime_ listens to." String address
100 | ) {
101 | this.vertx = vertx;
102 | this.address = address;
103 | }
104 |
105 | "Initializes the given list of extensions."
106 | Future initializeExtensions (
107 | "Configuration the _Chime_ is started with." JsonObject config,
108 | "List of the providers to be initialized." {Extension*} uninitialized,
109 | "Map to add successfully initialized providers."
110 | HashMap, HashMap>> toAddProviders
111 | ) {
112 | Integer total = uninitialized.size;
113 | if (total > 0) {
114 | variable Integer initialized = 0;
115 | Future ret = future.future();
116 |
117 | value added = (Extension|Throwable provider) {
118 | if (is Extension provider) {
119 | if (exists m = toAddProviders[provider.parameter]) {
120 | m.put(provider.type, provider);
121 | }
122 | else {
123 | value m = HashMap>();
124 | m.put(provider.type, provider);
125 | toAddProviders.put(provider.parameter, m);
126 | }
127 | }
128 | else {
129 | // TODO: log the extension hasn't been initialized
130 | }
131 | initialized ++;
132 | if (initialized >= total) {
133 | ret.complete();
134 | }
135 | };
136 |
137 | for (provider in uninitialized) {
138 | provider.initialize(vertx, config, added);
139 | }
140 | return ret;
141 | }
142 | else {
143 | return future.succeededFuture();
144 | }
145 | }
146 |
147 | "Initializes all external service providers."
148 | shared void initialize (
149 | "Configuration the _Chime_ is started with." JsonObject config,
150 | "Completion handler." Anything(Anything) complete
151 | ) {
152 | {String*} services = if (is JsonArray servicesArray = config.get(Chime.configuration.services))
153 | then servicesArray.narrow() else {};
154 | initializeExtensions(config, collectServices(services, `Extension`), providers).setHandler(complete);
155 | }
156 |
157 | "Returns info on the extensions."
158 | shared JsonArray extensionsInfo() {
159 | return JsonArray {
160 | for (m in providers)
161 | for (item in m.item.items)
162 | JsonObject {
163 | Chime.key.service -> m.key.string,
164 | item.type -> type(item).declaration.qualifiedName
165 | }
166 | };
167 | }
168 |
169 | shared actual Service|String> createService (
170 | String providerType, JsonObject options
171 | ) {
172 | if (is Extension service = providers[`Service`]?.get(providerType)) {
173 | return service.create(this, options);
174 | }
175 | else {
176 | return Chime.errors.codeUnsupportedServiceProviderType->Chime.errors.unsupportedServiceProviderType;
177 | }
178 | }
179 |
180 | shared actual TimeZone localTimeZone => JVMTimeZoneFactory.localTimeZone;
181 |
182 | }
183 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/Operator.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.json {
2 |
3 | JsonObject
4 | }
5 | import io.vertx.ceylon.core.eventbus {
6 |
7 | Message,
8 | EventBus,
9 | MessageConsumer
10 | }
11 | import java.util {
12 | UUID
13 | }
14 |
15 |
16 | "Provides basic operations with `JsonObject` message."
17 | see(`class SchedulerManager`, `class TimeScheduler`)
18 | since("0.1.0") by("Lis")
19 | abstract class Operator {
20 |
21 | "Generates unique string."
22 | shared static String uuidString() => UUID.randomUUID().string.replace(Chime.configuration.nameSeparator, ".");
23 |
24 | "Extracts state from request, helper method."
25 | shared static State? extractState(JsonObject request) {
26 | if (is String state = request[Chime.key.state]) {
27 | return stateByName(state);
28 | }
29 | else {
30 | return null;
31 | }
32 | }
33 |
34 |
35 | "Event bus consumer." variable MessageConsumer? consumer = null;
36 | "Address this operator listens to." shared String address;
37 | "EventBus to pass messages." shared EventBus eventBus;
38 |
39 |
40 | shared new (
41 | "Address this operator listens to." String address,
42 | "EventBus to pass messages." EventBus eventBus
43 | ) {
44 | this.address = address;
45 | this.eventBus = eventBus;
46 | }
47 |
48 |
49 | "Operators map."
50 | late Map)> operators = createOperators();
51 |
52 | "Creates operators map."
53 | shared formal Map)> createOperators();
54 |
55 |
56 | "Message has been received from event bus - process it!."
57 | void onMessage("Message from event bus." Message msg) {
58 | if (exists request = msg.body(), is String operation = request[Chime.key.operation]) {
59 | // depending on operation code
60 | if (exists operator = operators[operation]) {
61 | operator(msg);
62 | }
63 | else {
64 | msg.fail(Chime.errors.codeUnsupportedOperation, Chime.errors.unsupportedOperation);
65 | }
66 | }
67 | else {
68 | // response with wrong format error
69 | msg.fail(Chime.errors.codeOperationIsNotSpecified, Chime.errors.operationIsNotSpecified);
70 | }
71 | }
72 |
73 | "Connects to event bus, returns promise resolved when event listener registered."
74 | shared default void connect(Boolean local) {
75 | "Already connected."
76 | assert(!consumer exists);
77 | // setup event bus listener
78 | if (local) {
79 | consumer = eventBus.localConsumer(address, onMessage);
80 | }
81 | else {
82 | consumer = eventBus.consumer(address, onMessage);
83 | }
84 | }
85 |
86 | shared default void stop() {
87 | consumer?.unregister();
88 | consumer = null;
89 | }
90 |
91 | }
92 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/Scheduler.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.time {
2 | DateTime
3 | }
4 | import ceylon.json {
5 |
6 | JsonObject,
7 | JsonArray,
8 | ObjectValue
9 | }
10 |
11 |
12 | "Wraps event bus to provide exchanging messages with previously created scheduler.
13 | The object implementing interface is returned by [[connectScheduler]]."
14 | see(`interface Timer`, `function connectScheduler`, `function createScheduler`)
15 | tagged("Proxy")
16 | since("0.2.0") by("Lis")
17 | shared interface Scheduler {
18 |
19 | "Name of the scheduler.
20 | _Chime_ listens event bus at this address for the messages to this scheduler.
21 | See details in [[module herd.schedule.chime]]."
22 | shared formal String name;
23 |
24 | "Removes this scheduler."
25 | shared formal void delete("Optional reply handler. Replied with scheduler name." Anything(Throwable|String)? reply = null);
26 |
27 | "Pauses this scheduler."
28 | see(`function resume`)
29 | shared formal void pause("Optional reply handler. Replied with scheduler state." Anything(Throwable|State)? reply = null);
30 |
31 | "Resumes this scheduler after pausing."
32 | see(`function pause`)
33 | shared formal void resume("Optional reply handler. Replied with scheduler state." Anything(Throwable|State)? reply = null);
34 |
35 | "Requests scheduler info."
36 | see(`function schedulerInfo`)
37 | shared formal void info("Info handler." Anything(Throwable|SchedulerInfo) info);
38 |
39 | "Requests info on a list of timers."
40 | see(`function schedulerInfo`) since("0.2.1")
41 | shared formal void timersInfo (
42 | "Names of timers info is requested for." {String+} timers,
43 | "Info handler." Anything(Throwable|TimerInfo[]) info
44 | );
45 |
46 | "Deletes a list of timers.
47 | `handler` is called with a list of actually deleted timers or with an error if occured. "
48 | see(`function delete`) since("0.2.1")
49 | shared formal void deleteTimers (
50 | "Names of timers to be deleted." {String+} timers,
51 | "Optional delete handler." Anything(Throwable|{String*})? handler = null
52 | );
53 |
54 | "Creates an interval timer."
55 | shared default void createIntervalTimer (
56 | "Callback when timer created."
57 | Anything(Timer|Throwable) handler,
58 | "Interval timer delay in seconds."
59 | Integer delay,
60 | "Timer name. Timer address is timer full name, i.e. **scheduler name:timer name**.
61 | By default unique timer name is generate."
62 | String? timerName = null,
63 | "`True` if timer is paused at initial and `false` if running."
64 | Boolean paused = false,
65 | "Maximum number of fires or null if unlimited."
66 | Integer? maxCount = null,
67 | "Timer start date."
68 | DateTime? startDate = null,
69 | "Timer end date."
70 | DateTime? endDate = null,
71 | "Time zone, default is machine local."
72 | String? timeZone = null,
73 | "Optional time zone provider, default is \"jvm\"."
74 | String? timeZoneProvider = null,
75 | "Message to be passed to message source in order to extract final message."
76 | ObjectValue? message = null,
77 | "Optional message source type, default is \"direct\" which attaches `message` as is."
78 | String? messageSource = null,
79 | "Optional configuration passed to message source factory."
80 | JsonObject? messageSourceOptions = null,
81 | "Event producer provider."
82 | String? eventProducer = null,
83 | "Optional configuration passed to event producer factory."
84 | JsonObject? eventProducerOptions = null
85 | ) => createTimer (
86 | handler, JsonObject {Chime.key.type -> Chime.type.interval, Chime.key.delay -> delay},
87 | timerName, paused, maxCount, startDate, endDate, timeZone, timeZoneProvider,
88 | message, messageSource, messageSourceOptions, eventProducer, eventProducerOptions
89 | );
90 |
91 | "Creates a cron timer."
92 | shared default void createCronTimer (
93 | "Callback when timer created." Anything(Timer|Throwable) handler,
94 | "Seconds." String seconds,
95 | "Minutes." String minutes,
96 | "Hours." String hours,
97 | "Days of month." String daysOfMonth,
98 | "Months." String months,
99 | "Optional days of week." String? daysOfWeek = null,
100 | "Optional years." String? years = null,
101 | "Timer name. Timer address is timer full name, i.e. **scheduler name:timer name**.
102 | By default unique timer name is generate."
103 | String? timerName = null,
104 | "`True` if timer is paused at initial and `false` if running."
105 | Boolean paused = false,
106 | "Maximum number of fires or null if unlimited."
107 | Integer? maxCount = null,
108 | "Timer start date."
109 | DateTime? startDate = null,
110 | "Timer end date."
111 | DateTime? endDate = null,
112 | "Time zone."
113 | String? timeZone = null,
114 | "Optional time zone provider, default is \"jvm\"."
115 | String? timeZoneProvider = null,
116 | "Message to be passed to message source in order to extract final message."
117 | ObjectValue? message = null,
118 | "Optional message source type, default is \"direct\" which attaches `message` as is."
119 | String? messageSource = null,
120 | "Optional configuration passed to message source factory."
121 | JsonObject? messageSourceOptions = null,
122 | "Event producer provider."
123 | String? eventProducer = null,
124 | "Optional configuration passed to event producer factory."
125 | JsonObject? eventProducerOptions = null
126 | ) {
127 | JsonObject descr = JsonObject {
128 | Chime.key.type -> Chime.type.cron,
129 | Chime.date.seconds -> seconds,
130 | Chime.date.minutes -> minutes,
131 | Chime.date.hours -> hours,
132 | Chime.date.daysOfMonth -> daysOfMonth,
133 | Chime.date.months -> months
134 | };
135 | if (exists d = daysOfWeek, !d.empty) {
136 | descr.put(Chime.date.daysOfWeek, d);
137 | }
138 | if (exists d = years, !d.empty) {
139 | descr.put(Chime.date.years, d);
140 | }
141 | createTimer (
142 | handler, descr, timerName, paused, maxCount, startDate, endDate, timeZone,
143 | timeZoneProvider, message, messageSource, messageSourceOptions,
144 | eventProducer, eventProducerOptions
145 | );
146 | }
147 |
148 | "Creates an union timer."
149 | since( "0.2.1" )
150 | shared default void createUnionTimer (
151 | "Callback when timer created."
152 | Anything(Timer|Throwable) handler,
153 | "Nonempty list of the timers to be combined into union."
154 | {JsonObject+} timers,
155 | "Timer name. Timer address is timer full name, i.e. **scheduler name:timer name**.
156 | By default unique timer name is generate."
157 | String? timerName = null,
158 | "`True` if timer is paused at initial and `false` if running."
159 | Boolean paused = false,
160 | "Maximum number of fires or null if unlimited."
161 | Integer? maxCount = null,
162 | "Timer start date."
163 | DateTime? startDate = null,
164 | "Timer end date."
165 | DateTime? endDate = null,
166 | "Time zone."
167 | String? timeZone = null,
168 | "Optional time zone provider, default is \"jvm\"."
169 | String? timeZoneProvider = null,
170 | "Message to be passed to message source in order to extract final message."
171 | ObjectValue? message = null,
172 | "Optional message source type, default is \"direct\" which attaches `message` as is."
173 | String? messageSource = null,
174 | "Optional configuration passed to message source factory."
175 | JsonObject? messageSourceOptions = null,
176 | "Event producer provider."
177 | String? eventProducer = null,
178 | "Optional configuration passed to event producer factory."
179 | JsonObject? eventProducerOptions = null
180 | ) =>
181 | createTimer (
182 | handler, JsonObject {Chime.key.type -> Chime.type.union, Chime.key.timers -> JsonArray(timers)},
183 | timerName, paused, maxCount, startDate, endDate, timeZone, timeZoneProvider,
184 | message, messageSource, messageSourceOptions, eventProducer, eventProducerOptions
185 | );
186 |
187 |
188 | "Creates a timer with the given description."
189 | since( "0.2.1" )
190 | shared formal void createTimer (
191 | "Callback when timer created." Anything(Timer|Throwable) handler,
192 | "JSON timer description." JsonObject description,
193 | "Timer name. Timer address is timer full name, i.e. **scheduler name:timer name**.
194 | By default unique timer name is generate."
195 | String? timerName = null,
196 | "`True` if timer is paused at initial and `false` if running."
197 | Boolean paused = false,
198 | "Maximum number of fires or null if unlimited."
199 | Integer? maxCount = null,
200 | "Timer start date."
201 | DateTime? startDate = null,
202 | "Timer end date."
203 | DateTime? endDate = null,
204 | "Opyional time zone, default is scheduler or local."
205 | String? timeZone = null,
206 | "Optional time zone provider, default is scheduler or \"jvm\"."
207 | String? timeZoneProvider = null,
208 | "Message to be passed to message source in order to extract final message."
209 | ObjectValue? message = null,
210 | "Optional message source type, default is scheduler or \"direct\" which attaches `message` as is."
211 | String? messageSource = null,
212 | "Optional configuration passed to message source factory."
213 | JsonObject? messageSourceOptions = null,
214 | "Event producer provider."
215 | String? eventProducer = null,
216 | "Optional configuration passed to event producer factory."
217 | JsonObject? eventProducerOptions = null
218 | );
219 |
220 | }
221 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/SchedulerImpl.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.time {
2 | DateTime
3 | }
4 | import io.vertx.ceylon.core.eventbus {
5 | EventBus,
6 | Message
7 | }
8 | import ceylon.json {
9 | JsonObject,
10 | JsonArray,
11 | ObjectValue
12 | }
13 |
14 |
15 | "Internal implementation of [[Scheduler]]."
16 | see(`function connectScheduler`, `function createScheduler`)
17 | since("0.2.0") by("Lis")
18 | class SchedulerImpl
19 | extends Sender satisfies Scheduler
20 | {
21 |
22 | shared static void createSchedulerImpl(Anything(Throwable|Scheduler) handler, EventBus eventBus, Integer? sendTimeout)
23 | (Throwable|Message msg)
24 | {
25 | if (is Message msg) {
26 | "Reply from scheduler request has not to be null."
27 | assert (exists ret = msg.body());
28 | handler(SchedulerImpl(ret.getString(Chime.key.name), eventBus, sendTimeout));
29 | }
30 | else {
31 | handler(msg);
32 | }
33 | }
34 |
35 | shared static void replyWithInfo(Anything(Throwable|SchedulerInfo[]) handler)
36 | (Throwable|Message msg)
37 | {
38 | if (is Message msg) {
39 | "Reply from scheduler request has not to be null."
40 | assert (exists ret = msg.body());
41 | value sch = ret.getArray(Chime.key.schedulers);
42 | handler([for (item in sch.narrow()) SchedulerInfo.fromJSON(item)]);
43 | }
44 | else {
45 | handler( msg );
46 | }
47 | }
48 |
49 | shared static void replyWithList(Anything(Throwable|{String*}) handler, String key)
50 | (Throwable|Message msg)
51 | {
52 | if (is Message msg) {
53 | "Reply from scheduler request has not to be null."
54 | assert (exists ret = msg.body());
55 | handler (
56 | ret.getArray(Chime.key.schedulers).narrow()
57 | .chain(ret.getArray(Chime.key.timers).narrow())
58 | );
59 | }
60 | else {
61 | handler(msg);
62 | }
63 | }
64 |
65 | shared static void replyWithState(Anything(Throwable|State) handler)
66 | (Throwable|Message msg)
67 | {
68 | if (is Message msg) {
69 | "Reply from scheduler request has not to be null."
70 | assert (exists ret = msg.body());
71 | "Timer info replied from scheduler has to contain state field."
72 | assert (exists state = stateByName(ret.getString(Chime.key.state)));
73 | handler(state);
74 | }
75 | else {
76 | handler(msg);
77 | }
78 | }
79 |
80 | shared static void replyWithName(Anything(Throwable|String) handler)
81 | (Throwable|Message msg)
82 | {
83 | if (is Message msg) {
84 | "Reply from scheduler request has not to be null."
85 | assert (exists ret = msg.body());
86 | handler(ret.getString(Chime.key.name));
87 | }
88 | else {
89 | handler(msg);
90 | }
91 | }
92 |
93 | shared static void replyWithTimer (
94 | String? schedulerName, EventBus eventBus, Integer? sendTimeout, Anything(Throwable|TimerImpl) handler
95 | ) (Throwable | Message msg)
96 | {
97 | if (is Throwable msg) {
98 | handler(msg);
99 | }
100 | else {
101 | "Timer create request has to respond with body."
102 | assert (exists rep = msg.body());
103 | String name = rep.getString(Chime.key.name);
104 | "Timer full name has to contain scheduler and timer names."
105 | assert (exists inc = name.firstOccurrence(Chime.configuration.nameSeparatorChar));
106 | handler(TimerImpl(name, name.spanTo(inc - 1), eventBus, sendTimeout));
107 | }
108 | }
109 |
110 |
111 | variable Boolean alive = true;
112 | shared actual String name;
113 | EventBus eventBus;
114 | "Timeout to send message with." Integer? sendTimeout;
115 |
116 | shared new (String name, EventBus eventBus, "Timeout to send message with." Integer? sendTimeout)
117 | extends Sender(name, eventBus, sendTimeout)
118 | {
119 | this.name = name;
120 | this.eventBus = eventBus;
121 | this.sendTimeout = sendTimeout;
122 | }
123 |
124 | shared actual void createTimer (
125 | Anything(Timer|Throwable) handler, JsonObject description, String? timerName,
126 | Boolean paused, Integer? maxCount, DateTime? startDate,
127 | DateTime? endDate, String? timeZone, String? timeZoneProvider,
128 | ObjectValue? message, String? messageSource, JsonObject? messageSourceOptions,
129 | String? eventProducer, JsonObject? eventProducerOptions
130 |
131 | ) {
132 | JsonObject timer = JsonObject {
133 | Chime.key.operation -> Chime.operation.create
134 | };
135 | if (exists timerName) {
136 | timer.put( Chime.key.name, timerName );
137 | }
138 | if (paused) {
139 | timer.put(Chime.key.state, Chime.state.paused);
140 | }
141 | if (exists maxCount) {
142 | timer.put(Chime.key.maxCount, maxCount);
143 | }
144 | if (exists startDate) {
145 | timer.put (
146 | Chime.key.startTime,
147 | JsonObject {
148 | Chime.date.seconds -> startDate.seconds,
149 | Chime.date.minutes -> startDate.minutes,
150 | Chime.date.hours -> startDate.hours,
151 | Chime.date.dayOfMonth -> startDate.day,
152 | Chime.date.month -> startDate.month.integer,
153 | Chime.date.year -> startDate.year
154 | }
155 | );
156 | }
157 | if (exists endDate) {
158 | timer.put (
159 | Chime.key.endTime,
160 | JsonObject {
161 | Chime.date.seconds -> endDate.seconds,
162 | Chime.date.minutes -> endDate.minutes,
163 | Chime.date.hours -> endDate.hours,
164 | Chime.date.dayOfMonth -> endDate.day,
165 | Chime.date.month -> endDate.month.integer,
166 | Chime.date.year -> endDate.year
167 | }
168 | );
169 | }
170 | if (exists timeZone) {
171 | timer.put(Chime.key.timeZone, timeZone);
172 | }
173 | if (exists timeZoneProvider) {
174 | timer.put(Chime.key.timeZoneProvider, timeZoneProvider);
175 | }
176 | if (exists message) {
177 | timer.put(Chime.key.message, message);
178 | }
179 | if (exists messageSource) {
180 | timer.put( Chime.key.messageSource, messageSource );
181 | }
182 | if ( exists messageSourceOptions ) {
183 | timer.put( Chime.key.messageSourceOptions, messageSourceOptions );
184 | }
185 | if ( exists eventProducer ) {
186 | timer.put(Chime.key.eventProducer, eventProducer);
187 | }
188 | if (exists eventProducerOptions) {
189 | timer.put(Chime.key.eventProducerOptions, eventProducerOptions);
190 | }
191 | timer.put(Chime.key.description, description);
192 |
193 | sendRepliedRequest(timer, replyWithTimer(name, eventBus, sendTimeout, handler));
194 | }
195 |
196 |
197 | shared actual void delete(Anything(Throwable|String)? reply) {
198 | if (alive) {
199 | alive = false;
200 | if (exists reply) {
201 | sendRepliedRequest (
202 | JsonObject {
203 | Chime.key.operation -> Chime.operation.delete,
204 | Chime.key.name -> name
205 | },
206 | replyWithName(reply)
207 | );
208 | }
209 | else {
210 | sendRequest (
211 | JsonObject {
212 | Chime.key.operation -> Chime.operation.delete,
213 | Chime.key.name -> name
214 | }
215 | );
216 | }
217 | }
218 | }
219 |
220 | shared actual void pause( Anything(Throwable|State)? reply ) {
221 | if ( alive ) {
222 | if ( exists reply ) {
223 | sendRepliedRequest (
224 | JsonObject {
225 | Chime.key.operation -> Chime.operation.state,
226 | Chime.key.name -> name,
227 | Chime.key.state -> Chime.state.paused
228 | },
229 | replyWithState(reply)
230 | );
231 | }
232 | else {
233 | sendRequest (
234 | JsonObject {
235 | Chime.key.operation -> Chime.operation.state,
236 | Chime.key.name -> name,
237 | Chime.key.state -> Chime.state.paused
238 | }
239 | );
240 | }
241 | }
242 | }
243 |
244 | shared actual void resume(Anything(Throwable|State)? reply) {
245 | if (alive) {
246 | if (exists reply) {
247 | sendRepliedRequest (
248 | JsonObject {
249 | Chime.key.operation -> Chime.operation.state,
250 | Chime.key.name -> name,
251 | Chime.key.state -> Chime.state.running
252 | },
253 | replyWithState(reply)
254 | );
255 | }
256 | else {
257 | sendRequest (
258 | JsonObject {
259 | Chime.key.operation -> Chime.operation.state,
260 | Chime.key.name -> name,
261 | Chime.key.state -> Chime.state.running
262 | }
263 | );
264 | }
265 | }
266 | }
267 |
268 | shared actual void info(Anything(Throwable|SchedulerInfo) info) {
269 | sendRepliedRequest (
270 | JsonObject {
271 | Chime.key.operation -> Chime.operation.info
272 | },
273 | (Throwable|Message msg) {
274 | if (is Message msg) {
275 | "Reply from scheduler request has not to be null."
276 | assert(exists ret = msg.body());
277 | info(SchedulerInfo.fromJSON(ret));
278 | }
279 | else {
280 | info(msg);
281 | }
282 | }
283 | );
284 | }
285 |
286 | shared actual void deleteTimers({String+} timers, Anything(Throwable|{String*})? handler) {
287 | if (exists handler) {
288 | sendRepliedRequest (
289 | JsonObject {
290 | Chime.key.operation -> Chime.operation.delete,
291 | Chime.key.name -> JsonArray(timers)
292 | },
293 | replyWithList(handler, Chime.key.timers)
294 | );
295 | }
296 | else {
297 | sendRequest (
298 | JsonObject {
299 | Chime.key.operation -> Chime.operation.delete,
300 | Chime.key.name -> JsonArray(timers)
301 | }
302 | );
303 | }
304 | }
305 |
306 | shared actual void timersInfo({String+} timers, Anything(Throwable|TimerInfo[]) info) {
307 | sendRepliedRequest (
308 | JsonObject {
309 | Chime.key.operation -> Chime.operation.info,
310 | Chime.key.name -> JsonArray(timers)
311 | },
312 | TimerImpl.replyWithInfo(info)
313 | );
314 | }
315 |
316 | shared actual String string => "Scheduler ``name``";
317 |
318 | }
319 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/SchedulerInfo.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.json {
2 | JsonObject
3 | }
4 |
5 |
6 | "Info on the scheduler."
7 | see(`interface Scheduler`, `class TimerInfo`, `function schedulerInfo`)
8 | tagged("Info")
9 | since("0.2.0") by("Lis")
10 | shared final class SchedulerInfo {
11 |
12 | "Scheduler name." shared String name;
13 | "Scheduler state at the request moment." shared State state;
14 | "Default time zone." shared String timeZone;
15 | "List of the timers. Actual at the request moment." shared TimerInfo[] timers;
16 |
17 | "Instantiates `SchedulerInfo` with the given parameters."
18 | shared new (
19 | "Scheduler name." String name,
20 | "Scheduler state at the request moment." State state,
21 | "Default time zone the scheduler." String timeZone,
22 | "List of the timers. Actual at the request moment." TimerInfo[] timers
23 | ) {
24 | this.name = name;
25 | this.state = state;
26 | this.timeZone = timeZone;
27 | this.timers = timers;
28 | }
29 |
30 | "Instantiates `SchedulerInfo` from JSON description as send by _Chime_."
31 | shared new fromJSON("Scheduler info received from _Chime_." JsonObject schedulerInfo) {
32 | this.name = schedulerInfo.getString(Chime.key.name);
33 | "Scheduler info replied from _Chime_ has to contain state field."
34 | assert( exists state = stateByName(schedulerInfo.getString(Chime.key.state)));
35 | this.state = state;
36 | this.timeZone = schedulerInfo.getString(Chime.key.timeZone);
37 | if (exists arr = schedulerInfo.getArrayOrNull(Chime.key.timers)) {
38 | timers = arr.narrow().map(TimerInfo.fromJSON).sequence();
39 | }
40 | else {
41 | timers = [];
42 | }
43 | }
44 |
45 | shared actual String string => "Info on scheduler ``name``, ``state``";
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/Sender.ceylon:
--------------------------------------------------------------------------------
1 | import io.vertx.ceylon.core.eventbus {
2 | EventBus,
3 | DeliveryOptions,
4 | Message
5 | }
6 | import ceylon.json {
7 | JsonObject
8 | }
9 |
10 |
11 | "Base class for event bus senders."
12 | since("0.3.0") by("Lis")
13 | class Sender (
14 | "Address to send." shared String address,
15 | "EB" shared EventBus eventBus,
16 | "Timeout to send message with." shared Integer? sendTimeout
17 | ) {
18 |
19 | DeliveryOptions? options = if (exists sendTimeout) then DeliveryOptions(null, null, sendTimeout) else null;
20 |
21 | shared void sendRequest(JsonObject request) {
22 | if (exists options) {
23 | eventBus.send(address, request, options);
24 | }
25 | else {
26 | eventBus.send(address, request);
27 | }
28 | }
29 |
30 | shared void sendRepliedRequest(JsonObject request, Anything(Throwable|Message) rep) {
31 | if (exists options) {
32 | eventBus.send(address, request, options, rep);
33 | }
34 | else {
35 | eventBus.send(address, request, rep);
36 | }
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/State.ceylon:
--------------------------------------------------------------------------------
1 |
2 | "Returns timer state by name:
3 | * \"paused\" - timerPaused
4 | * \"running\" - timerRunning
5 | * \"completed\" - timerCompleted
6 | * otherwise - null
7 | "
8 | since( "0.2.0" ) by( "Lis" )
9 | State? stateByName( String name ) {
10 | if ( name == State.paused.string ) {
11 | return State.paused;
12 | }
13 | else if ( name == State.running.string ) {
14 | return State.running;
15 | }
16 | else if ( name == State.completed.string ) {
17 | return State.completed;
18 | }
19 | else {
20 | return null;
21 | }
22 | }
23 |
24 |
25 | "Timer or scheduler state - running, paused or completed."
26 | tagged( "Info" )
27 | since( "0.2.0" ) by( "Lis" )
28 | shared class State of running | paused | completed
29 | {
30 |
31 | shared actual String string;
32 |
33 | "Indicates that the scheduler or timer is in running state."
34 | shared new running { string = Chime.state.running; }
35 |
36 | "Indicates that the scheduler or timer is in paused state."
37 | shared new paused { string = Chime.state.paused; }
38 |
39 | "Indicates that the scheduler or timer has been completed."
40 | shared new completed { string = Chime.state.completed; }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/TimeServices.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.json {
2 | ObjectValue
3 | }
4 | import herd.schedule.chime.service.message {
5 | MessageSource
6 | }
7 | import ceylon.time {
8 | DateTime
9 | }
10 | import herd.schedule.chime.service.timezone {
11 | TimeZone
12 | }
13 | import herd.schedule.chime.service.producer {
14 | EventProducer
15 | }
16 |
17 |
18 | "Services for the timer or scheduler."
19 | since("0.3.0") by("Lis")
20 | final class TimeServices (
21 | shared TimeZone timeZone,
22 | shared MessageSource messageSource,
23 | shared EventProducer eventProducer,
24 | shared CalendarService calendar
25 | )
26 | satisfies TimeZone&MessageSource&EventProducer&CalendarService
27 | {
28 | shared actual void extract(TimerFire event, Anything(ObjectValue?) onMessage)
29 | => messageSource.extract(event, onMessage);
30 |
31 | shared actual Boolean inside(DateTime date) => calendar.inside(date);
32 |
33 | shared actual DateTime nextOutside(DateTime date) => calendar.nextOutside(date);
34 |
35 | shared actual void send(TimerEvent event) => eventProducer.send(event);
36 |
37 | shared actual String timeZoneID => timeZone.timeZoneID;
38 |
39 | shared actual DateTime toLocal(DateTime remote) => timeZone.toLocal(remote);
40 |
41 | shared actual DateTime toRemote(DateTime local) => timeZone.toRemote(local);
42 |
43 | shared actual Boolean calendarIgnorance => calendar.calendarIgnorance;
44 | }
45 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/TimeUnit.ceylon:
--------------------------------------------------------------------------------
1 |
2 |
3 | "Unit of time."
4 | since( "0.2.1" ) by( "Lis" )
5 | shared class TimeUnit of hours | minutes | seconds
6 | {
7 | "Number of seconds in the unit."
8 | shared Integer secondsIn;
9 |
10 | "Unit of hour."
11 | shared new hours { secondsIn = 3600; }
12 | "Unit of minutes."
13 | shared new minutes { secondsIn = 60; }
14 | "Unit of seconds."
15 | shared new seconds { secondsIn = 1; }
16 | }
17 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/Timer.ceylon:
--------------------------------------------------------------------------------
1 |
2 |
3 | "Wraps event bus to provide exchanging messages with previously created timer.
4 | The object implementing interface is returned by [[Scheduler.createIntervalTimer]]
5 | [[Scheduler.createCronTimer]], [[Scheduler.createUnionTimer]] and [[Scheduler.createTimer]].
6 |
7 | Timer is sent timer fire or complete events with [[TimerEvent]].
8 | To set timer event handler call [[handler]].
9 |
10 | > Complete event is always published.
11 |
12 | If a timer object is no longer needed call [[unregister]] in order to unregister event listener at event bus.
13 | The listener is automatically unregistered at timer complete event.
14 | "
15 | see( `interface Scheduler` )
16 | tagged( "Proxy" )
17 | since( "0.2.0" ) by( "Lis" )
18 | shared interface Timer {
19 |
20 | "Full name of the timer, i.e. **scheduler name:timer name**."
21 | shared formal String name;
22 |
23 | "Stops and removes this timer."
24 | see( `function Scheduler.deleteTimers` )
25 | shared formal void delete( "Optional reply handler. Replied with timer name." Anything(Throwable|String)? reply = null );
26 |
27 | "Pauses this timer."
28 | see( `function resume` )
29 | shared formal void pause( "Optional reply handler. Replied with timer state." Anything(Throwable|State)? reply = null );
30 |
31 | "Resumes this timer after pausing."
32 | see( `function pause` )
33 | shared formal void resume( "Optional reply handler. Replied with timer state." Anything(Throwable|State)? reply = null );
34 |
35 | "Requests timer info."
36 | see( `function Scheduler.timersInfo` )
37 | shared formal void info( "Info handler." Anything(Throwable|TimerInfo) info );
38 |
39 | "Sets the handler for the timer events. Replaces previous one if has been set."
40 | shared formal void handler( Anything(TimerEvent) handler );
41 |
42 | "Unregister the handler from the event bus, while keep the timer alive."
43 | shared formal void unregister();
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/TimerContainer.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.json {
2 |
3 | JsonObject,
4 | ObjectValue
5 | }
6 | import ceylon.time {
7 |
8 | DateTime
9 | }
10 | import herd.schedule.chime.service.timer {
11 | TimeRow
12 | }
13 |
14 |
15 | "Posses timer."
16 | since("0.1.0") by("Lis")
17 | class TimerContainer (
18 | "Timer full name, which is **scheduler name:timer name**." shared String name,
19 | "Timer `JsonObject` description" JsonObject description,
20 | "Timer within this container." TimeRow timer,
21 | "Max count or null if not specified." Integer? maxCount,
22 | "Timer start time or null if to be started immediately." DateTime? startTime,
23 | "Timer end time or null if not specified." DateTime? endTime,
24 | "Message to be attached to the timer fire event." ObjectValue? message,
25 | "Timer services: time zone, message source, event producer, calendar."
26 | TimeServices services
27 | ) {
28 |
29 | "Timer fire counting."
30 | shared variable Integer count = 0;
31 |
32 | "Timer state."
33 | shared variable State state = State.paused;
34 |
35 | "Next fire timer in remote TZ or null if completed."
36 | variable DateTime? nextRemoteFireTime = null;
37 |
38 | "Next fire timer in machine local TZ or null if completed."
39 | shared DateTime? localFireTime => if (exists d = nextRemoteFireTime) then services.toLocal(d) else null;
40 |
41 | "Time zone ID."
42 | shared String timeZoneID => services.timeZoneID;
43 |
44 |
45 | "Timer name + state."
46 | shared JsonObject stateDescription() {
47 | return JsonObject {
48 | Chime.key.name -> name,
49 | Chime.key.state -> state.string
50 | };
51 | }
52 |
53 | "Returns _full_ timer description."
54 | shared JsonObject fullDescription() {
55 | JsonObject descr = JsonObject {
56 | Chime.key.name -> name,
57 | Chime.key.state -> state.string,
58 | Chime.key.count -> count,
59 | Chime.key.description -> description,
60 | Chime.key.timeZone -> timeZoneID
61 | };
62 |
63 | if (exists d = maxCount) {
64 | descr.put(Chime.key.maxCount, d);
65 | }
66 |
67 | if (exists d = startTime) {
68 | descr.put (
69 | Chime.key.startTime,
70 | JsonObject {
71 | Chime.date.seconds -> d.seconds,
72 | Chime.date.minutes -> d.minutes,
73 | Chime.date.hours -> d.hours,
74 | Chime.date.dayOfMonth -> d.day,
75 | Chime.date.month -> d.month.string,
76 | Chime.date.year -> d.year
77 | }
78 | );
79 | }
80 |
81 | if (exists d = endTime) {
82 | descr.put (
83 | Chime.key.endTime,
84 | JsonObject {
85 | Chime.date.seconds -> d.seconds,
86 | Chime.date.minutes -> d.minutes,
87 | Chime.date.hours -> d.hours,
88 | Chime.date.dayOfMonth -> d.day,
89 | Chime.date.month -> d.month.string,
90 | Chime.date.year -> d.year
91 | }
92 | );
93 | }
94 |
95 | return descr;
96 | }
97 |
98 | "Creates timer fire event for the next fire date time and using extracted message."
99 | shared void timerFireEvent() {
100 | if (state == State.running, exists at = nextRemoteFireTime) {
101 | TimerFire event = TimerFire(name, count, timeZoneID, at, message);
102 | services.extract(event, sendTimerFireEvent(event));
103 | }
104 | }
105 |
106 | void sendTimerFireEvent(TimerFire event)(ObjectValue? message)
107 | => services.send(TimerFire(event.timerName, event.count, event.timeZone, event.date, message));
108 |
109 |
110 | "Starts the timer."
111 | shared void start(DateTime currentLocal) {
112 | DateTime currentRemote = services.toRemote(currentLocal);
113 | // check if max count has been reached before
114 | if (exists c = maxCount) {
115 | if (count >= c) {
116 | complete();
117 | return;
118 | }
119 | }
120 | // check if start time is after current
121 | DateTime beginning;
122 | if (exists st = startTime) {
123 | if (st > currentRemote) {
124 | beginning = st;
125 | }
126 | else {
127 | beginning = currentRemote;
128 | }
129 | }
130 | else {
131 | beginning = currentRemote;
132 | }
133 | // start timer
134 | if (exists date = calendarDate(timer.start(beginning))) {
135 | if (exists ed = endTime) {
136 | if (date > ed) {
137 | complete();
138 | return;
139 | }
140 | }
141 | state = State.running;
142 | count ++;
143 | nextRemoteFireTime = date;
144 | }
145 | else {
146 | complete();
147 | }
148 | }
149 |
150 | "Sets timer completed."
151 | shared void complete() {
152 | nextRemoteFireTime = null;
153 | state = State.completed;
154 | services.send(TimerCompleted(name, count));
155 | }
156 |
157 | "Shifts timer to the next time."
158 | shared void shiftTime() {
159 | if (state == State.running) {
160 | if (exists date = calendarDate(timer.shiftTime())) {
161 | // check on complete
162 | if (exists ed = endTime) {
163 | if (date > ed) {
164 | complete();
165 | return;
166 | }
167 | }
168 | if (exists c = maxCount) {
169 | if (count >= c) {
170 | complete();
171 | return;
172 | }
173 | }
174 | count ++;
175 | nextRemoteFireTime = date;
176 | }
177 | else {
178 | complete();
179 | }
180 | }
181 | }
182 |
183 | "Returns date bounds by calendar."
184 | DateTime? calendarDate(variable DateTime? date) {
185 | while (exists cur = date) {
186 | if (services.inside(cur)) {
187 | if (services.calendarIgnorance) {
188 | date = timer.shiftTime();
189 | }
190 | else {
191 | date = services.nextOutside(cur);
192 | }
193 | }
194 | else {
195 | return cur;
196 | }
197 | }
198 | return null;
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/TimerCreator.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.json {
2 |
3 | JsonObject
4 | }
5 | import ceylon.time {
6 |
7 | dateTime,
8 | DateTime
9 | }
10 | import herd.schedule.chime.service {
11 | ChimeServices
12 | }
13 | import herd.schedule.chime.service.timer {
14 | TimeRowFactory,
15 | TimeRow
16 | }
17 | import herd.schedule.chime.cron {
18 |
19 | calendar
20 | }
21 |
22 |
23 | "Uses `JSON` description to creates [[TimerContainer]] with timer [[TimeRow]] created by timer factory."
24 | see(`interface TimeRowFactory`, `interface TimeRow`, `class TimerContainer`)
25 | since("0.1.0") by("Lis")
26 | class TimerCreator (
27 | "Services Chime provides." ChimeServices services
28 | ) {
29 |
30 | "Creates timer from creation request."
31 | shared TimerContainer|String> createTimer (
32 | "Timer name." String name,
33 | "Request with timer description." JsonObject request,
34 | "Default time services." TimeServices defaultServices
35 | ) {
36 | if (is JsonObject description = request[Chime.key.description]) {
37 | value timer = services.createTimeRow(description);
38 | if (is TimeRow timer) {
39 | return createTimerContainer(request, description, name, timer, defaultServices);
40 | }
41 | else {
42 | return timer;
43 | }
44 | }
45 | else {
46 | // timer description to be specified
47 | return Chime.errors.codeTimerDescriptionHasToBeSpecified->Chime.errors.timerDescriptionHasToBeSpecified;
48 | }
49 | }
50 |
51 |
52 | "Creates timer container by container and creation request."
53 | TimerContainer|String> createTimerContainer (
54 | "Request on timer creation." JsonObject request,
55 | "Timer description." JsonObject description,
56 | "Timer name." String name,
57 | "Timer." TimeRow timer,
58 | "Default time services." TimeServices defaultServices
59 | ) {
60 | // extract start date if exists
61 | DateTime? startDate;
62 | if (is JsonObject startTime = request[Chime.key.startTime]) {
63 | if (exists st = extractDate(startTime)) {
64 | startDate = st;
65 | }
66 | else {
67 | return Chime.errors.codeIncorrectStartDate->Chime.errors.incorrectStartDate;
68 | }
69 | }
70 | else {
71 | startDate = null;
72 | }
73 |
74 | // extract end date if exists
75 | DateTime? endDate;
76 | if (is JsonObject endTime = request[Chime.key.endTime]) {
77 | if (exists st = extractDate(endTime)) {
78 | endDate = st;
79 | }
80 | else {
81 | return Chime.errors.codeIncorrectEndDate->Chime.errors.incorrectEndDate;
82 | }
83 | }
84 | else {
85 | endDate = null;
86 | }
87 |
88 | // end date has to be after start!
89 | if (exists st = startDate, exists et = endDate) {
90 | if (et <= st) {
91 | return Chime.errors.codeEndDateToBeAfterStartDate->Chime.errors.endDateToBeAfterStartDate;
92 | }
93 | }
94 |
95 | value exactServices = servicesFromRequest (
96 | request, services, defaultServices
97 | );
98 | if (is TimeServices exactServices) {
99 | return TimerContainer (
100 | name, description, timer,
101 | extractMaxCount(request), startDate, endDate,
102 | request.get(Chime.key.message), exactServices
103 | );
104 | }
105 | else {
106 | return exactServices;
107 | }
108 | }
109 |
110 | "Extracts month from field with key key. The field can be either integer or string (like JAN, FEB etc, see [[calendar]])."
111 | Integer? extractMonth(JsonObject description, String key) {
112 | if (is Integer val = description[key]) {
113 | if (val > 0 && val < 13) {
114 | return val;
115 | }
116 | else {
117 | return null;
118 | }
119 | }
120 | else if (is String val = description[key]) {
121 | if (exists ret = calendar.monthFullMap[val]) {
122 | return ret;
123 | }
124 | return calendar.monthShortMap[val];
125 | }
126 | else {
127 | return null;
128 | }
129 | }
130 |
131 | "Extracts date from `JSON`, key returns `JSON` object with date."
132 | DateTime? extractDate(JsonObject date) {
133 | if (is Integer seconds = date[Chime.date.seconds],
134 | is Integer minutes = date[Chime.date.minutes],
135 | is Integer hours = date[Chime.date.hours],
136 | is Integer dayOfMonth = date[Chime.date.dayOfMonth],
137 | is Integer year = date[Chime.date.year],
138 | exists month = extractMonth(date, Chime.date.month)
139 | ) {
140 | try {
141 | return dateTime(year, month, dayOfMonth, hours, minutes, seconds);
142 | }
143 | catch (Throwable err) {
144 | return null;
145 | }
146 | }
147 | return null;
148 | }
149 |
150 | "`maxCount` - nonmandatory field, if not specified - infinitely."
151 | Integer? extractMaxCount(JsonObject description) {
152 | if (is Integer c = description[Chime.key.maxCount]) {
153 | if (c > 0) {
154 | return c;
155 | }
156 | else {
157 | return 1;
158 | }
159 | }
160 | else {
161 | return null;
162 | }
163 | }
164 |
165 | }
166 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/TimerEvent.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.time {
2 | DateTime
3 | }
4 | import ceylon.json {
5 | JsonObject,
6 | ObjectValue
7 | }
8 |
9 |
10 | "Represents a timer event: fire or complete.
11 | Timer publishes or sends the event in `JSON` format to timer address when the timer fires or completes.
12 | [[Timer]] interface converts `JsonObject` event into `TimerEvent`.
13 |
14 | > Complete event is always published.
15 | "
16 | see(`interface Timer`, `function Timer.handler`)
17 | tagged("Event")
18 | since("0.2.0") by("Lis")
19 | shared abstract class TimerEvent (
20 | "Name of the timer which sent the message."
21 | shared String timerName,
22 | "Total number of fires."
23 | shared Integer count
24 | )
25 | of TimerFire | TimerCompleted
26 | {
27 | "Writes the event into `JsonObject`."
28 | shared formal JsonObject toJson();
29 | }
30 |
31 |
32 | "Timer fire event."
33 | see(`interface Timer`, `function Timer.handler`)
34 | tagged("Event")
35 | since("0.2.0") by("Lis")
36 | shared final class TimerFire (
37 | "Name of the timer which fires the message."
38 | String timerName,
39 | "Total number of fires."
40 | Integer count,
41 | "Time zone ID."
42 | shared String timeZone,
43 | "Date the fire is occured at."
44 | shared DateTime date,
45 | "Optional message attached to the timer fire event."
46 | shared ObjectValue? message
47 | )
48 | extends TimerEvent(timerName, count)
49 | {
50 |
51 | shared actual JsonObject toJson() {
52 | value ret = JsonObject {
53 | Chime.key.event -> Chime.event.fire,
54 | Chime.key.name -> timerName,
55 | Chime.key.count -> count,
56 | Chime.key.time -> date.string,
57 | Chime.date.seconds -> date.seconds,
58 | Chime.date.minutes -> date.minutes,
59 | Chime.date.hours -> date.hours,
60 | Chime.date.dayOfMonth -> date.day,
61 | Chime.date.month -> date.month.integer,
62 | Chime.date.year -> date.year,
63 | Chime.key.timeZone -> timeZone
64 | };
65 | if ( exists msg = message ) {
66 | ret.put( Chime.key.message, msg );
67 | }
68 | return ret;
69 | }
70 |
71 |
72 | shared actual String string {
73 | String ret = "fire event of ``timerName`` at ``date`` with total fires of ``count``";
74 | if (exists msg = message) {
75 | return ret + " and message ``msg``";
76 | }
77 | else {
78 | return ret;
79 | }
80 | }
81 | }
82 |
83 |
84 | "Timer complete event."
85 | see(`interface Timer`, `function Timer.handler`)
86 | tagged("Event")
87 | since("0.2.0") by("Lis")
88 | shared final class TimerCompleted (
89 | "Name of the timer which fires the message."
90 | String timerName,
91 | "Total number of fires."
92 | Integer count
93 | )
94 | extends TimerEvent(timerName, count)
95 | {
96 | shared actual JsonObject toJson()
97 | => JsonObject {
98 | Chime.key.event -> Chime.event.complete,
99 | Chime.key.name -> timerName,
100 | Chime.key.count -> count
101 | };
102 | shared actual String string => "complete event of ``timerName`` with total fires of ``count``";
103 | }
104 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/TimerImpl.ceylon:
--------------------------------------------------------------------------------
1 | import io.vertx.ceylon.core.eventbus {
2 | EventBus,
3 | MessageConsumer,
4 | Message
5 | }
6 | import ceylon.json {
7 | JsonObject
8 | }
9 |
10 |
11 | "Internal implementation of [[Timer]]."
12 | see(`interface Scheduler`, `class SchedulerImpl`)
13 | since("0.2.0") by("Lis")
14 | class TimerImpl extends Sender satisfies Timer
15 | {
16 |
17 | shared static void replyWithInfo( Anything(Throwable|TimerInfo[]) info)
18 | (Throwable|Message msg)
19 | {
20 | if (is Message msg) {
21 | "Reply from scheduler request has not to be null."
22 | assert (exists ret = msg.body());
23 | info(ret.getArray(Chime.key.timers).narrow().map(TimerInfo.fromJSON).sequence());
24 | }
25 | else {
26 | info(msg);
27 | }
28 | }
29 |
30 |
31 | variable Anything(TimerEvent)? eventHandler = null;
32 | MessageConsumer consumer;
33 | variable Boolean alive = true;
34 | shared actual String name;
35 |
36 | shared new (String name, String schedulerAddress, EventBus eventBus, Integer? sendTimeout)
37 | extends Sender(schedulerAddress, eventBus, sendTimeout)
38 | {
39 | this.name = name;
40 | consumer = eventBus.consumer(name);
41 | }
42 |
43 |
44 | "Redirects message to `eventHandler`."
45 | void onMessage(Message message) {
46 | if (exists h = eventHandler) {
47 | "Message from timer has to containe body."
48 | assert (exists body = message.body());
49 | value eventType = body.getString(Chime.key.event);
50 | if (eventType == Chime.event.fire) {
51 | h (
52 | TimerFire (
53 | name, body.getInteger(Chime.key.count),
54 | body.getString(Chime.key.timeZone),
55 | dateTimeFromJSON(body),
56 | body.get(Chime.key.message)
57 | )
58 | );
59 | }
60 | else if (eventType == Chime.event.complete) {
61 | unregister();
62 | alive = false;
63 | h(TimerCompleted(name, body.getInteger(Chime.key.count)));
64 | }
65 | else {
66 | throw AssertionError("timer event has to be one of 'fire' or 'complete'");
67 | }
68 | }
69 | }
70 |
71 |
72 | shared actual void handler(Anything(TimerEvent) handler) {
73 | if (alive) {
74 | eventHandler = handler;
75 | if (!consumer.isRegistered()) {
76 | consumer.handler(onMessage);
77 | }
78 | }
79 | }
80 |
81 | shared actual void unregister() {
82 | consumer.unregister();
83 | eventHandler = null;
84 | }
85 |
86 | shared actual void delete(Anything(Throwable|String)? reply) {
87 | if (alive) {
88 | unregister();
89 | alive = false;
90 | if (exists reply) {
91 | sendRepliedRequest (
92 | JsonObject {
93 | Chime.key.operation -> Chime.operation.delete,
94 | Chime.key.name -> name
95 | },
96 | SchedulerImpl.replyWithName(reply)
97 | );
98 | }
99 | else {
100 | sendRequest (
101 | JsonObject {
102 | Chime.key.operation -> Chime.operation.delete,
103 | Chime.key.name -> name
104 | }
105 | );
106 | }
107 | }
108 | }
109 |
110 | shared actual void pause(Anything(Throwable|State)? reply) {
111 | if (alive) {
112 | if (exists reply) {
113 | sendRepliedRequest (
114 | JsonObject {
115 | Chime.key.operation -> Chime.operation.state,
116 | Chime.key.name -> name,
117 | Chime.key.state -> Chime.state.paused
118 | },
119 | SchedulerImpl.replyWithState(reply)
120 | );
121 | }
122 | else {
123 | sendRequest (
124 | JsonObject {
125 | Chime.key.operation -> Chime.operation.state,
126 | Chime.key.name -> name,
127 | Chime.key.state -> Chime.state.paused
128 | }
129 | );
130 | }
131 | }
132 | }
133 |
134 | shared actual void resume(Anything(Throwable|State)? reply) {
135 | if (alive) {
136 | if (exists reply) {
137 | sendRepliedRequest (
138 | JsonObject {
139 | Chime.key.operation -> Chime.operation.state,
140 | Chime.key.name -> name,
141 | Chime.key.state -> Chime.state.running
142 | },
143 | SchedulerImpl.replyWithState(reply)
144 | );
145 | }
146 | else {
147 | sendRequest (
148 | JsonObject {
149 | Chime.key.operation -> Chime.operation.state,
150 | Chime.key.name -> name,
151 | Chime.key.state -> Chime.state.running
152 | }
153 | );
154 | }
155 | }
156 | }
157 |
158 | shared actual void info("Info handler." Anything(Throwable|TimerInfo) info) {
159 | sendRepliedRequest (
160 | JsonObject {
161 | Chime.key.operation -> Chime.operation.info,
162 | Chime.key.name -> name
163 | },
164 | (Throwable|Message msg) {
165 | if (is Message msg) {
166 | "Reply from scheduler request has not to be null."
167 | assert (exists ret = msg.body());
168 | info( TimerInfo.fromJSON( ret ));
169 | }
170 | else {
171 | info(msg);
172 | }
173 | }
174 | );
175 | }
176 |
177 | shared actual String string => "Timer ``name``";
178 |
179 | }
180 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/TimerInfo.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.json {
2 |
3 | JsonObject
4 | }
5 | import ceylon.time {
6 |
7 | DateTime
8 | }
9 |
10 |
11 | "Info on the timer."
12 | see(`interface Timer`, `class SchedulerInfo`, `function Scheduler.info`)
13 | tagged("Info")
14 | since("0.2.0") by("Lis")
15 | shared final class TimerInfo {
16 |
17 | "Timer name" shared String name;
18 | "Timer state at the request moment." shared State state;
19 | "Number of fires at the request moment." shared Integer count;
20 | "Maximum allowed number of fires or `null` if unlimited." shared Integer? maxCount;
21 | "Time the timer has to be started, or `null` if immediately." shared DateTime? startTime;
22 | "Optional time the timer has to be completed." shared DateTime? endTime;
23 | "Time zone the timer works in." shared String timeZone;
24 | "Timer description." shared JsonObject description;
25 |
26 |
27 | "Instantiates `TimerInfo` with the given parameters."
28 | shared new (
29 | "Timer name" String name,
30 | "Timer state at the request moment." State state,
31 | "Number of fires at the request moment." Integer count,
32 | "Maximum allowed number of fires or `null` if unlimited." Integer? maxCount,
33 | "Time the timer has to be started, or `null` if immediately." DateTime? startTime,
34 | "Optional time the timer has to be completed." DateTime? endTime,
35 | "Time zone the timer works in." String timeZone,
36 | "Timer description." JsonObject description
37 | ) {
38 | this.name = name;
39 | this.state = state;
40 | this.count = count;
41 | this.maxCount = maxCount;
42 | this.startTime = startTime;
43 | this.endTime = endTime;
44 | this.timeZone = timeZone;
45 | this.description = description;
46 | }
47 |
48 | "Instantiates `TimerInfo` from `JsonObject` description as send by _Chime_."
49 | shared new fromJSON("Timer info received from _Chime_." JsonObject timerInfo) {
50 | this.name = timerInfo.getString(Chime.key.name);
51 | "Timer info replied from scheduler has to contain state field."
52 | assert (exists state = stateByName(timerInfo.getString(Chime.key.state)));
53 | this.state = state;
54 | this.count = timerInfo.getInteger(Chime.key.count);
55 | this.maxCount = timerInfo.getIntegerOrNull(Chime.key.maxCount);
56 | this.startTime =
57 | if (exists startTimeDescr = timerInfo.getObjectOrNull(Chime.key.startTime))
58 | then dateTimeFromJSON(startTimeDescr)
59 | else null;
60 | this.endTime =
61 | if (exists endTimeDescr = timerInfo.getObjectOrNull(Chime.key.endTime))
62 | then dateTimeFromJSON(endTimeDescr)
63 | else null;
64 | this.timeZone = timerInfo.getString(Chime.key.timeZone);
65 | this.description = timerInfo.getObject(Chime.key.description);
66 | }
67 |
68 | "Name of the scheduler the timer works within."
69 | shared String schedulerName => name[... (name.firstOccurrence( Chime.configuration.nameSeparatorChar) else 0) - 1];
70 |
71 | "Short name of the timer, i.e. name with shceduler name skipped."
72 | shared String shortName => name[(name.firstOccurrence(Chime.configuration.nameSeparatorChar) else 0) + 1 ...];
73 |
74 | shared actual String string => "Info on timer ``name``, ``state``, count = ``count``";
75 |
76 | }
77 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/UnionBuilder.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.collection {
2 | ArrayList
3 | }
4 | import ceylon.json {
5 |
6 | JsonObject,
7 | JsonArray
8 | }
9 | import ceylon.time {
10 | DateTime,
11 | Time
12 | }
13 | import ceylon.time.base {
14 | DayOfWeek,
15 | Month
16 | }
17 | import herd.schedule.chime.cron {
18 | calendar,
19 | cron
20 | }
21 |
22 |
23 | "Builds an union timer.
24 | The builder has a number of function to add particular timer.
25 | The function may be called in any order and any number of times.
26 | Finally, [[UnionBuilder.build]] has to be called to build the timer `JsonObject` description.
27 | "
28 | tagged("Builder")
29 | see(`class CronBuilder`, `function package.every`)
30 | since("0.2.1") by("Lis")
31 | shared final class UnionBuilder
32 | {
33 |
34 | ArrayList union;
35 |
36 |
37 | "Instantiates new empty cron builder."
38 | shared new () {
39 | union = ArrayList();
40 | }
41 |
42 | "Instatiates new builder and copy data from `other`."
43 | shared new fromBuilder("Builder to copy data from." UnionBuilder other) {
44 | union = ArrayList{for(item in other.union) item.clone()};
45 | }
46 |
47 |
48 | "Builds the timer."
49 | shared JsonObject build()
50 | => JsonObject {
51 | Chime.key.type -> Chime.type.union,
52 | Chime.key.timers -> JsonArray(union)
53 | };
54 |
55 |
56 | "Adds timer by its `JSON` description."
57 | shared void withTimer("Timer description to be added." JsonObject timer) => union.add(timer);
58 |
59 | "Fires at the given date / time with year taken into account.
60 | So, this timer will fire just a once."
61 | shared void at("Date / time to fire at." DateTime time)
62 | => union.add (
63 | JsonObject {
64 | Chime.key.type -> Chime.type.cron,
65 | Chime.date.seconds -> time.seconds.string,
66 | Chime.date.minutes -> time.minutes.string,
67 | Chime.date.hours -> time.hours.string,
68 | Chime.date.daysOfMonth -> time.day.string,
69 | Chime.date.months -> time.month.string
70 | }
71 | );
72 |
73 | "Fires every year at the given time, day and month."
74 | shared void annual (
75 | "Time to fire at." Time time,
76 | "Day of month to fire at." Integer dayOfMonth,
77 | "Month to fire at." Integer|String|Month month,
78 | "Optional list of days of week to limit fire event to.
79 | > Sunday is the first day of week."
80 | {Integer|String|DayOfWeek+} daysOfWeek = 1..7
81 | ) => union.add (
82 | JsonObject {
83 | Chime.key.type -> Chime.type.cron,
84 | Chime.date.seconds -> time.seconds.string,
85 | Chime.date.minutes -> time.minutes.string,
86 | Chime.date.hours -> time.hours.string,
87 | Chime.date.daysOfMonth -> dayOfMonth.string,
88 | Chime.date.months -> calendar.digitalMonth(month).string,
89 | Chime.date.daysOfWeek -> calendar.cronDaysOfWeek(daysOfWeek),
90 | Chime.date.years -> cron.allValues.string
91 | }
92 | );
93 |
94 | "Fires every month at the given time and day of month.
95 | I.e. `monthly(time(0,0,0), 1)` will fire each 1st day of every month at 0:0:0."
96 | shared void monthly (
97 | "Time to fire at." Time time,
98 | "Day of month to fire at." Integer dayOfMonth,
99 | "Optional list of days of week to limit fire event to.
100 | > Sunday is the first day of week."
101 | {Integer|String|DayOfWeek+} daysOfWeek = 1..7
102 | ) => union.add (
103 | JsonObject {
104 | Chime.key.type -> Chime.type.cron,
105 | Chime.date.seconds -> time.seconds.string,
106 | Chime.date.minutes -> time.minutes.string,
107 | Chime.date.hours -> time.hours.string,
108 | Chime.date.daysOfMonth -> dayOfMonth.string,
109 | Chime.date.months -> cron.allValues.string,
110 | Chime.date.daysOfWeek -> calendar.cronDaysOfWeek(daysOfWeek),
111 | Chime.date.years -> cron.allValues.string
112 | }
113 | );
114 |
115 | "Fires every given day of week at the given time.
116 | I.e. `weekly(time(0,0,0), monday)` will fire each monday at 0:0:0 time."
117 | shared void weekly (
118 | "Time to fire at." Time time,
119 | "Day of week to fire at.
120 | > Sunday is the first day of week."
121 | Integer|String|DayOfWeek dayOfWeek
122 | ) => union.add (
123 | JsonObject {
124 | Chime.key.type -> Chime.type.cron,
125 | Chime.date.seconds -> time.seconds.string,
126 | Chime.date.minutes -> time.minutes.string,
127 | Chime.date.hours -> time.hours.string,
128 | Chime.date.daysOfMonth -> cron.allValues.string,
129 | Chime.date.months -> cron.allValues.string,
130 | Chime.date.daysOfWeek -> calendar.digitalDayOfWeek(dayOfWeek).string,
131 | Chime.date.years -> cron.allValues.string
132 | }
133 | );
134 |
135 | "Fires every month at the last day of week and given time.
136 | I.e. `last(time(0,0,0), friday)` will fire every month last friday at 0:0:0 time."
137 | shared void last (
138 | "Time to fire at." Time time,
139 | "Last day of week to fire at.
140 | > Sunday is the first day of week."
141 | Integer|String|DayOfWeek dayOfWeek
142 | ) => union.add (
143 | JsonObject {
144 | Chime.key.type -> Chime.type.cron,
145 | Chime.date.seconds -> time.seconds.string,
146 | Chime.date.minutes -> time.minutes.string,
147 | Chime.date.hours -> time.hours.string,
148 | Chime.date.daysOfMonth -> cron.allValues.string,
149 | Chime.date.months -> cron.allValues.string,
150 | Chime.date.daysOfWeek -> calendar.digitalDayOfWeek(dayOfWeek).string + cron.last.string,
151 | Chime.date.years -> cron.allValues.string
152 | }
153 | );
154 |
155 | "Fires every month at the n'th day of week and given time.
156 | I.e. `last(time(0,0,0), friday, 3)` will fire every month third friday at 0:0:0 time."
157 | shared void ordered (
158 | "Time to fire at." Time time,
159 | "Day of week to fire at.
160 | > Sunday is the first day of week."
161 | Integer|String|DayOfWeek dayOfWeek,
162 | "Theorder of the day of week to fire at." Integer order
163 | ) => union.add (
164 | JsonObject {
165 | Chime.key.type -> Chime.type.cron,
166 | Chime.date.seconds -> time.seconds.string,
167 | Chime.date.minutes -> time.minutes.string,
168 | Chime.date.hours -> time.hours.string,
169 | Chime.date.daysOfMonth -> cron.allValues.string,
170 | Chime.date.months -> cron.allValues.string,
171 | Chime.date.daysOfWeek -> calendar.digitalDayOfWeek(dayOfWeek).string + cron.nth.string + order.string,
172 | Chime.date.years -> cron.allValues.string
173 | }
174 | );
175 |
176 | "Fires daily at the given time.
177 | For example, `daily(Time(12,0,0), 1..5)` will fire at 12:0:0 at working days only."
178 | shared void daily (
179 | "Time to fire at." Time time,
180 | "Optional list of days of week to limit fire event to.
181 | > Sunday is the first day of week."
182 | {Integer|String|DayOfWeek+} daysOfWeek = 1..7
183 | ) => union.add (
184 | JsonObject {
185 | Chime.key.type -> Chime.type.cron,
186 | Chime.date.seconds -> time.seconds.string,
187 | Chime.date.minutes -> time.minutes.string,
188 | Chime.date.hours -> time.hours.string,
189 | Chime.date.daysOfMonth -> cron.allValues.string,
190 | Chime.date.months -> cron.allValues.string,
191 | Chime.date.daysOfWeek -> calendar.cronDaysOfWeek(daysOfWeek),
192 | Chime.date.years -> cron.allValues.string
193 | }
194 | );
195 |
196 | "Fires every given time interval."
197 | shared void every (
198 | "Timer interval measured in `timeUnit`." Integer interval,
199 | "Unit to measure `delay`." TimeUnit timeUnit = TimeUnit.seconds
200 | ) {
201 | "Timer interval has to be positive, while given is ``interval``."
202 | assert(interval > 0);
203 | union.add (
204 | JsonObject {
205 | Chime.key.type -> Chime.type.interval,
206 | Chime.key.delay -> interval * timeUnit.secondsIn
207 | }
208 | );
209 | }
210 |
211 | }
212 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/calendarBuilder.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.time.base {
2 | Month,
3 | DayOfWeek
4 | }
5 | import ceylon.json {
6 | JsonObject,
7 | JsonArray
8 | }
9 | import herd.schedule.chime.service.calendar {
10 | AnnuallyFactory,
11 | DailyFactory,
12 | DativityFactory,
13 | LastDayOfWeekFactory,
14 | MonthlyFactory,
15 | WeekdayOfMonthFactory,
16 | WeeklyFactory,
17 | IntersectionFactory,
18 | UnionFactory
19 | }
20 | import ceylon.time {
21 | Time
22 | }
23 |
24 |
25 | "Builds calendar Json descriptions."
26 | tagged("Builder")
27 | see(`package herd.schedule.chime.service.calendar`)
28 | since("0.3.0") by("Lis")
29 | shared object calendar
30 | {
31 |
32 | "Builds annually calendar Json desciption.
33 | Annually calendar excludes a given list of months."
34 | see(`class AnnuallyFactory`)
35 | shared JsonObject annually("Months the calendar excludes." Month+ months)
36 | => JsonObject {
37 | Chime.calendar.type -> Chime.calendar.annually,
38 | Chime.date.months -> JsonArray {for (item in months) item.integer}
39 | };
40 |
41 | "Builds daily calendar Json description.
42 | Daily calendar excludes _from - to_ time of day."
43 | see(`class DailyFactory`)
44 | shared JsonObject daily (
45 | "Time the calendar excludes from." Time from,
46 | "Time the calendar excludes to." Time to,
47 | "Tolerance in second applied to shift the calendar out the restrictions, default is 1s." Integer tolerance = 1
48 | ) {
49 | value ret = JsonObject {
50 | Chime.calendar.type -> Chime.calendar.daily,
51 | Chime.calendar.from -> JsonObject {
52 | Chime.date.seconds -> from.seconds,
53 | Chime.date.minutes -> from.minutes,
54 | Chime.date.hours -> from.hours
55 | },
56 | Chime.calendar.to -> JsonObject {
57 | Chime.date.seconds -> to.seconds,
58 | Chime.date.minutes -> to.minutes,
59 | Chime.date.hours -> to.hours
60 | }
61 | };
62 | if (tolerance > 0) {
63 | ret.put(Chime.calendar.tolerance, tolerance);
64 | }
65 | return ret;
66 | }
67 |
68 | "Builds dativity calendar Json desciption.
69 | Dativity calendar excludes a given list of dates."
70 | see(`class DativityFactory`)
71 | shared JsonObject dativity (
72 | "Dates calendar excludes.
73 | If it is `[Integer, Month]` then date with the given day and month and with any year is excluded.
74 | If it is `[Integer, Month, Integer]` then date with the given day, month and year is excluded"
75 | [Integer, Month]|[Integer, Month, Integer]+ dates
76 | )
77 | => JsonObject {
78 | Chime.calendar.type -> Chime.calendar.dativity,
79 | Chime.calendar.dates -> JsonArray {
80 | for (item in dates) if (is [Integer, Month] item) then
81 | JsonObject {
82 | Chime.date.dayOfMonth -> item[0],
83 | Chime.date.month -> item[1].integer
84 | }
85 | else
86 | JsonObject {
87 | Chime.date.dayOfMonth -> item[0],
88 | Chime.date.month -> item[1].integer,
89 | Chime.date.year -> item[2]
90 | }
91 | }
92 | };
93 |
94 | "Builds last day of week calendar Json description.
95 | Last day of week calendar excludes only a last day of week in each month."
96 | see(`class LastDayOfWeekFactory`)
97 | shared JsonObject lastDayOfWeek("Day of week calenda excludes. Only last day of week." DayOfWeek dayOfWeek)
98 | => JsonObject {
99 | Chime.calendar.type -> Chime.calendar.lastDayOfWeek,
100 | Chime.date.dayOfWeek -> dayOfWeek.successor.integer
101 | };
102 |
103 | "Builds monthly calendar Json description.
104 | Monthly calendar excludes a given list of days for every month."
105 | see(`class MonthlyFactory`)
106 | shared JsonObject monthly("Days calendar excludes." Integer+ days)
107 | => JsonObject {
108 | Chime.calendar.type -> Chime.calendar.monthly,
109 | Chime.date.months -> JsonArray(days)
110 | };
111 |
112 | "Builds weekday of month calendar Json description.
113 | Weekday of month calendar excludes a given weekday with the given order in each month (i.e. excludes nth weekday).
114 | "
115 | see(`class WeekdayOfMonthFactory`)
116 | shared JsonObject weekdayOfMonth (
117 | "Day of week to be excluded." DayOfWeek dayOfWeek,
118 | "Order of the excluded day of week." Integer order
119 | )
120 | => JsonObject {
121 | Chime.calendar.type -> Chime.calendar.weekdayOfMonth,
122 | Chime.date.dayOfWeek -> dayOfWeek.successor.integer,
123 | Chime.date.order -> order
124 | };
125 |
126 | "Builds weekly calendar Json description.
127 | Weekly calendar excludes a given list of week days every week."
128 | see(`class WeeklyFactory`)
129 | shared JsonObject weekly (
130 | "Days of week to be excluded." DayOfWeek+ dayOfWeeks
131 | )
132 | => JsonObject {
133 | Chime.calendar.type -> Chime.calendar.monthly,
134 | Chime.date.months -> JsonArray {for(item in dayOfWeeks) item.successor.integer}
135 | };
136 |
137 | "Builds calendars intersection."
138 | see(`class IntersectionFactory`)
139 | shared JsonObject intersect(JsonObject+ calendars)
140 | => JsonObject {
141 | Chime.calendar.type -> Chime.calendar.intersection,
142 | Chime.date.months -> JsonArray(calendars)
143 | };
144 |
145 | "Builds calendars union."
146 | see(`class UnionFactory`)
147 | shared JsonObject union(JsonObject+ calendars)
148 | => JsonObject {
149 | Chime.calendar.type -> Chime.calendar.union,
150 | Chime.date.months -> JsonArray(calendars)
151 | };
152 |
153 | }
154 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/cron/CronExpression.ceylon:
--------------------------------------------------------------------------------
1 |
2 | "Cron expression parsed by items."
3 | since( "0.1.0" ) by( "Lis" )
4 | shared class CronExpression (
5 | "Set of exoression seconds, 0-59." shared Set seconds,
6 | "Set of exoression minutes, 0-59." shared Set minutes,
7 | "Set of exoression hours, 0-23." shared Set hours,
8 | "Set of exoression days of month, 1-31." shared Set daysOfMonth,
9 | "Set of exoression months, 1-12." shared Set months,
10 | "Days of week." shared DaysOfWeekList daysOfWeek,
11 | "Set of exoression years, can be empty." shared Set years
12 | )
13 | {}
14 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/cron/DaysOrder.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.time {
2 |
3 | Date
4 | }
5 | import ceylon.time.base {
6 |
7 | DayOfWeek
8 | }
9 |
10 |
11 | "Checks order of day of week."
12 | since("0.1.0") by("Lis")
13 | shared interface DayOrder
14 | {
15 | "`true` if data falls on a one of ordered day and `false` otherwise."
16 | shared formal Boolean falls(Date date);
17 | }
18 |
19 |
20 | "Cheks if date falls on a one from day of week list."
21 | since("0.1.0") by("Lis")
22 | shared class DaysOfWeekList("set of day order" shared {DayOrder*} orderedDays) satisfies DayOrder
23 | {
24 | "`true` if one of ordered days returns `true` and `false` if all of them returns `false`."
25 | shared actual Boolean falls(Date date) {
26 | for (item in orderedDays) {
27 | if (item.falls(date)) {
28 | return true;
29 | }
30 | }
31 | return false;
32 | }
33 | }
34 |
35 |
36 | "All days are accepted."
37 | since("0.1.0") by("Lis")
38 | class DayOrderAll() satisfies DayOrder
39 | {
40 | shared actual Boolean falls(Date date) => true;
41 | }
42 |
43 | "Checks if data falls on one of specified day of week."
44 | since("0.1.0") by("Lis")
45 | class DayOrderWeek("Set of accepted days of week, if empty all days are rejected." shared Set daysOfWeek)
46 | satisfies DayOrder
47 | {
48 | shared actual Boolean falls(Date date) => daysOfWeek.contains(date.dayOfWeek);
49 | }
50 |
51 | "Checks if date is nth day of week."
52 | since("0.1.0") by("Lis")
53 | class DayOrderNth("Accepted day of week." shared DayOfWeek day, "'nth' order of day of week." Integer order)
54 | satisfies DayOrder
55 | {
56 | shared actual Boolean falls(Date date) => date.dayOfWeek == day && order == (date.day - 1) / 7 + 1;
57 | }
58 |
59 | "Checks if date is last day of week in the month."
60 | since("0.1.0") by("Lis")
61 | class DayOrderLast("Accepted day of week." shared DayOfWeek day)
62 | satisfies DayOrder
63 | {
64 | shared actual Boolean falls(Date date) => date.dayOfWeek == day && date.plusDays(7).month != date.month;
65 | }
66 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/cron/calendar.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.time.base {
2 | DayOfWeek,
3 | Month,
4 | weekdays
5 | }
6 | import ceylon.json {
7 | JsonObject
8 | }
9 |
10 |
11 | "Defines calendar constants."
12 | since( "0.1.0" ) by( "Lis" )
13 | shared object calendar
14 | {
15 |
16 | "Mapping month short name to month id."
17 | shared Map monthShortMap =
18 | map {
19 | "JAN" -> 1,
20 | "FEB" -> 2,
21 | "MAR" -> 3,
22 | "APR" -> 4,
23 | "MAY" -> 5,
24 | "JUN" -> 6,
25 | "JUL" -> 7,
26 | "AUG" -> 8,
27 | "SEP" -> 9,
28 | "OCT" -> 10,
29 | "NOV" -> 11,
30 | "DEC" -> 12
31 | };
32 |
33 | "Mapping month name to month id."
34 | shared Map monthFullMap =
35 | map {
36 | "JANUARY" -> 1,
37 | "FEBRUARY" -> 2,
38 | "MARCH" -> 3,
39 | "APRIL" -> 4,
40 | "MAY" -> 5,
41 | "JUNE" -> 6,
42 | "JULY" -> 7,
43 | "AUGUST" -> 8,
44 | "SEPTEMBER" -> 9,
45 | "OCTOBER" -> 10,
46 | "NOVEMBER" -> 11,
47 | "DECEMBER" -> 12
48 | };
49 |
50 | "Mapping day of week short name to day of week id."
51 | shared Map dayOfWeekShortMap =
52 | map {
53 | "SUN" -> 1,
54 | "MON" -> 2,
55 | "TUE" -> 3,
56 | "WED" -> 4,
57 | "THU" -> 5,
58 | "FRI" -> 6,
59 | "SAT" -> 7
60 | };
61 |
62 | "Mapping day of week name to day of week id."
63 | shared Map dayOfWeekFullMap =
64 | map {
65 | "SUNDAY" -> 1,
66 | "MONDAY" -> 2,
67 | "TUESDAY" -> 3,
68 | "WEDNESDAY" -> 4,
69 | "THURSDAY" -> 5,
70 | "FRIDAY" -> 6,
71 | "SATURDAY" -> 7
72 | };
73 |
74 |
75 | String replaceStringToNumber(String expression, Map map) {
76 | variable String ret = expression;
77 | for (key -> item in map) {
78 | ret = ret.replace(key, item.string);
79 | }
80 | return ret;
81 | }
82 |
83 | "Replace all occurancies of month names by corresponding number."
84 | shared String replaceMonthByNumber(String expression)
85 | => replaceStringToNumber(replaceStringToNumber(expression.trimmed.uppercased, monthFullMap), monthShortMap);
86 |
87 | "Replace all occurancies of weekday names by corresponding number."
88 | shared String replaceDayOfWeekByNumber(String expression)
89 | => replaceStringToNumber(replaceStringToNumber(expression.trimmed.uppercased, dayOfWeekFullMap), dayOfWeekShortMap);
90 |
91 |
92 | "Integer representation of a day of week."
93 | shared Integer digitalDayOfWeek(Integer|DayOfWeek|String dayOfWeek) {
94 | switch (dayOfWeek)
95 | case (is Integer) {
96 | "Has to be a valid day of week. Actually is ``dayOfWeek``."
97 | assert (dayOfWeek > 0 && dayOfWeek < 8);
98 | return dayOfWeek;
99 | }
100 | case (is DayOfWeek) {
101 | return dayOfWeek.successor.integer;
102 | }
103 | case (is String) {
104 | "Has to be a valid day of week. Actually is ``dayOfWeek``."
105 | assert (is Integer ret = Integer.parse(replaceDayOfWeekByNumber(dayOfWeek)));
106 | return ret;
107 | }
108 | }
109 |
110 | "Day of week from Integer or String."
111 | shared DayOfWeek dayOfWeekFromString(Integer|String dayOfWeek) {
112 | "Has to be a valid day of week. Actually is ``dayOfWeek``."
113 | assert (exists dow = weekdays[digitalDayOfWeek(dayOfWeek) - 1] );
114 | return dow;
115 | }
116 |
117 | "Day of week from json."
118 | shared DayOfWeek dayOfWeekFromJson(JsonObject descr, String key) {
119 | "Day of week has to be of String or Integer."
120 | assert (is Integer|String m = descr[key]);
121 | return dayOfWeekFromString(m);
122 | }
123 |
124 | "Integer representation of a list of day of week."
125 | shared {Integer+} digitalDaysOfWeekList({Integer|DayOfWeek|String+} daysOfWeek) {
126 | return {for (item in daysOfWeek) digitalDayOfWeek(item)};
127 | }
128 |
129 | shared String cronDaysOfWeek({Integer|DayOfWeek|String+} daysOfWeek) {
130 | StringBuilder builder = StringBuilder();
131 | for (item in daysOfWeek.exceptLast) {
132 | builder.append(digitalDayOfWeek(item).string + ",");
133 | }
134 | builder.append(digitalDayOfWeek(daysOfWeek.last).string);
135 | return builder.string;
136 | }
137 |
138 | "Integer month from json."
139 | shared Integer monthFromJson(JsonObject descr, String key) {
140 | "Month has to be of String or Integer."
141 | assert (is Integer|String m = descr[key]);
142 | return digitalMonth(m);
143 | }
144 |
145 | "Integer representation of a month."
146 | shared Integer digitalMonth(Integer|Month|String month) {
147 | switch (month)
148 | case (is Integer) {
149 | "Has to be a valid month. Actually is ``month``."
150 | assert (month > 0 && month < 13);
151 | return month;
152 | }
153 | case (is Month) {
154 | return month.integer;
155 | }
156 | case (is String) {
157 | "Has to be a valid month. Actually is ``month``."
158 | assert (is Integer ret = Integer.parse(calendar.replaceMonthByNumber(month)));
159 | return ret;
160 | }
161 | }
162 |
163 | }
164 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/cron/cronDefinitions.ceylon:
--------------------------------------------------------------------------------
1 |
2 | "defines some constant used within cron expresion"
3 | since( "0.1.0" ) by( "Lis" )
4 | shared object cron
5 | {
6 |
7 | // cron special symbols
8 |
9 | "separators"
10 | shared {Character*} separators = { ' ', '\t', '\r', '\n' };
11 |
12 | "delimiter of fields"
13 | shared Character delimiter = ',';
14 |
15 | "special symbols in a one token"
16 | shared {Character*} special = { '/', '-' };
17 |
18 | "increments symbol"
19 | shared Character increments = '/';
20 |
21 | "range symbol"
22 | shared Character range = '-';
23 |
24 | "all values symbol"
25 | shared Character allValues = '*';
26 |
27 | "last symbol"
28 | shared Character last = 'L';
29 |
30 | "nth day symbol"
31 | shared Character nth = '#';
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/cron/package.ceylon:
--------------------------------------------------------------------------------
1 | "Parsing cron strings."
2 | since( "0.1.0" ) by( "Lis" )
3 | package herd.schedule.chime.cron;
4 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/cron/parseCron.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.time {
2 |
3 | today
4 | }
5 |
6 |
7 | "Parses cron expression from strings."
8 | since( "0.1.0" ) by( "Lis" )
9 | shared CronExpression? parseCron (
10 | "cron style string with seconds" String seconds,
11 | "cron style string with minutes" String minutes,
12 | "cron style string with hours" String hours,
13 | "cron style string with daysOfMonth" String daysOfMonth,
14 | "cron style string with months" String months,
15 | "cron style string with days of week - optional if not specified all weekdays included" String? daysOfWeek = null,
16 | "cron style string with years - optional if not specified every year included" String? years = null,
17 | "maximum year period." Integer maxYearPeriod = 10
18 | ) {
19 |
20 | // replace month names by numbers
21 | variable String monthsToInt = calendar.replaceMonthByNumber( months );
22 |
23 | // parse mandatory fields
24 | value secondsSet = parseCronStyle( seconds, 0, 59 );
25 | value minutesSet = parseCronStyle( minutes, 0, 59 );
26 | value hoursSet = parseCronStyle( hours, 0, 23 );
27 | value daysOfMonthSet = parseCronStyle( daysOfMonth, 1, 31 );
28 | value monthsSet = parseCronStyle( monthsToInt, 1, 12 );
29 |
30 | if ( !secondsSet.empty && !minutesSet.empty && !hoursSet.empty && !daysOfMonthSet.empty && !monthsSet.empty ) {
31 |
32 | // parse days of week, which is nonmandatory, if doesn't exists all days accepted
33 | DaysOfWeekList daysOfWeekList;
34 | if ( exists strDaysOfWeek = daysOfWeek, !strDaysOfWeek.empty ) {
35 | // replace all weekday names by numbers
36 | variable String weekdayToInt = calendar.replaceDayOfWeekByNumber( strDaysOfWeek );
37 | // do parsing
38 | if ( exists parsedDaysOfWeek = parseCronDaysOfWeek( weekdayToInt ) ) {
39 | daysOfWeekList = parsedDaysOfWeek;
40 | }
41 | else {
42 | return null;
43 | }
44 | }
45 | else {
46 | daysOfWeekList = DaysOfWeekList( {DayOrderAll()} );
47 | }
48 |
49 | // parse years, which is nonmandatory, if doesn't exists any year accepted
50 | Set yearsSet;
51 | if ( exists strYears = years, !strYears.empty ) {
52 | Integer todayYear = today().year;
53 | yearsSet = parseCronStyle( strYears, todayYear, todayYear + maxYearPeriod );
54 | if ( yearsSet.empty ) {
55 | return null;
56 | }
57 | }
58 | else {
59 | yearsSet = emptySet;
60 | }
61 |
62 | return CronExpression( secondsSet, minutesSet, hoursSet, daysOfMonthSet, monthsSet, daysOfWeekList, yearsSet );
63 | }
64 | else {
65 | return null;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/cron/parseCronDaysOfWeek.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.collection {
2 |
3 | ArrayList,
4 | HashSet,
5 | linked,
6 | Hashtable
7 | }
8 | import ceylon.time.base {
9 |
10 | dayOfWeek,
11 | DayOfWeek
12 | }
13 |
14 |
15 | "Parses days of week from string."
16 | since( "0.1.0" ) by( "Lis" )
17 | DaysOfWeekList? parseCronDaysOfWeek( String expression ) {
18 |
19 | // all values
20 | if ( expression == cron.allValues.string || expression.empty ) {
21 | return DaysOfWeekList( {DayOrderAll()} );
22 | }
23 |
24 | ArrayList days = ArrayList();
25 | // parse tokens
26 | {String*} tokens = expression.split( cron.delimiter.equals ).map( String.trimmed );
27 | for ( token in tokens ) {
28 | if ( exists parsedToken = parseDayOrder( token ) ) {
29 | days.add( parsedToken );
30 | }
31 | else {
32 | return null;
33 | }
34 | }
35 |
36 | if ( days.empty ) {
37 | return null;
38 | }
39 | else {
40 | return DaysOfWeekList( { for ( item in days ) item } );
41 | }
42 |
43 | }
44 |
45 |
46 | "Parses day order from string."
47 | by( "Lis" )
48 | DayOrder? parseDayOrder( String expression ) {
49 | if ( expression.contains( cron.nth ) ) {
50 | {String*} tokens = expression.split( cron.nth.equals );
51 | if ( tokens.size == 2,
52 | exists weekday = parseStringToInteger( tokens.first ),
53 | exists order = parseStringToInteger( tokens.last )
54 | ) {
55 | if ( weekday > 0 && weekday < 8 && order > 0 && order < 6 ) {
56 | return DayOrderNth( dayOfWeek( weekday - 1 ), order );
57 | }
58 | }
59 | }
60 | else if ( exists last = expression.last, last == cron.last ) {
61 | if ( exists day = parseStringToInteger( expression.spanTo( expression.size - 2 ) ) ) {
62 | if ( day > 0 && day < 8 ) {
63 | return DayOrderLast( dayOfWeek( day - 1 ) );
64 | }
65 | }
66 | }
67 | else {
68 | if ( exists daysSet = parseCronRange( expression, 1, 7 ) ) {
69 | return DayOrderWeek (
70 | HashSet (
71 | linked, Hashtable(), daysSet.map( ( Integer element ) => dayOfWeek( element - 1 ) )
72 | )
73 | );
74 | }
75 | }
76 | return null;
77 | }
78 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/cron/parseCronRange.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.collection {
2 |
3 | HashSet
4 | }
5 |
6 |
7 | "Parses cron range in format TO-FROM / STEP
8 | where TO, FROM and STEP:
9 | * contains only digits,
10 | * `FROM`-`TO`/`STEP`, `FROM`, `TO` and `STEP` are digits
11 | * `FROM`/`STEP`, `FROM` and `STEP` are digits, TO eqauls to max possible value
12 | * `FROM`-`TO`, FROM and TO are digits, step is supposed to be 1
13 | * TO and FROM have to be greater or equal min and less or equal max
14 | "
15 | since( "0.1.0" ) by( "Lis" )
16 | Set? parseCronRange( String expression, Integer minValue, Integer maxValue ) {
17 | HashSet ret = HashSet();
18 | {String*} ranged = expression.split( cron.special.contains, false ).map( String.trimmed );
19 | if ( exists from = parseStringToInteger( ranged.first ) ) {
20 | variable Integer to = from;
21 | variable Integer step = 1;
22 | if ( ranged.size == 1 ) {
23 | if ( from < minValue || from > maxValue ) {
24 | // 'from' to be within accepted values
25 | return null;
26 | }
27 | }
28 | else if ( ranged.size == 3 ) {
29 | if ( exists del = ranged.getFromFirst( 1 ) ) {
30 | if ( del == cron.range.string ) {
31 | if ( exists parsed = parseStringToInteger( ranged.getFromFirst( 2 ) ) ) {
32 | to = parsed;
33 | if ( to < from || to > maxValue ) {
34 | // 'to' to be within accepted values
35 | return null;
36 | }
37 | }
38 | else {
39 | // not digits
40 | return null;
41 | }
42 | }
43 | else if ( del == cron.increments.string ) {
44 | if ( exists parsed = parseStringToInteger( ranged.getFromFirst( 2 ) ) ) {
45 | if ( parsed < 1 ) {
46 | // step to be greater zero
47 | return null;
48 | }
49 | step = parsed;
50 | to = maxValue;
51 | }
52 | else {
53 | // not digits
54 | return null;
55 | }
56 | }
57 | else {
58 | // only '-' or '/' supported
59 | return null;
60 | }
61 | }
62 | else {
63 | return null;
64 | }
65 | } else if ( ranged.size == 5 ) {
66 | if ( exists del1 = ranged.getFromFirst( 1 ), exists del2 = ranged.getFromFirst( 3 ) ) {
67 | if ( del1 == cron.range.string && del2 == cron.increments.string ) {
68 | if ( exists parsedTo = parseStringToInteger( ranged.getFromFirst( 2 ) ),
69 | exists parsedStep = parseStringToInteger( ranged.getFromFirst( 4 ) ) )
70 | {
71 | if ( parsedTo < minValue || parsedTo > maxValue || parsedStep < 1 ) {
72 | // incorrect values
73 | return null;
74 | }
75 | to = parsedTo;
76 | step = parsedStep;
77 | }
78 | else {
79 | // not digits
80 | return null;
81 | }
82 | }
83 | else {
84 | // incorrect format - to be X-X/X
85 | return null;
86 | }
87 | }
88 | else {
89 | return null;
90 | }
91 | }
92 | else {
93 | // token must be in format X-X/X
94 | return null;
95 | }
96 | if ( step < 1 ) {
97 | step = 1;
98 | }
99 | // store range into set
100 | variable Integer storing = from;
101 | while ( storing <= to && storing <= maxValue ) {
102 | if ( storing >= minValue ) {
103 | ret.add( storing );
104 | }
105 | storing += step;
106 | }
107 | if ( ret.empty ) {
108 | return null;
109 | }
110 | else {
111 | return ret;
112 | }
113 | }
114 | return null;
115 | }
116 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/cron/parseCronStyle.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.collection {
2 |
3 | HashSet
4 | }
5 |
6 |
7 | "Parses string expression with cron style time, returns set with all possible values.
8 | Supported time in format X,X,...
9 | where X to be:
10 | * digits greater or equal min and less or equal max,
11 | * `FROM`-`TO`/`STEP`, `FROM`, `TO` and `STEP` are digits
12 | * `FROM`/`STEP`, `FROM` and `STEP` are digits, TO eqauls to max possible value
13 | * `FROM`-`TO`, FROM and TO are digits, step is supposed to be 1
14 | "
15 | since( "0.1.0" ) by( "Lis" )
16 | Set parseCronStyle(
17 | "expression to be parsed" String expression,
18 | "min possible value" Integer minValue,
19 | "max possible value" Integer maxValue )
20 | {
21 | String trimmedExpr = expression.trimmed;
22 | HashSet ret = HashSet();
23 |
24 | // all values
25 | if ( trimmedExpr == cron.allValues.string ) {
26 | variable Integer storing = minValue;
27 | while ( storing <= maxValue ) {
28 | ret.add( storing );
29 | storing ++;
30 | }
31 | return ret;
32 | }
33 |
34 | // parse tokens
35 | {String*} tokens = trimmedExpr.split( cron.delimiter.equals ).map( String.trimmed );
36 | for ( token in tokens ) {
37 | if ( exists tokenSet = parseCronRange( token, minValue, maxValue ) ) {
38 | ret.addAll( tokenSet );
39 | }
40 | else {
41 | ret.clear();
42 | break;
43 | }
44 | }
45 | return ret;
46 | }
47 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/cron/stringToInteger.ceylon:
--------------------------------------------------------------------------------
1 |
2 | "Parses Integer from String?."
3 | since( "0.1.0" ) by( "Lis" )
4 | Integer? parseStringToInteger( String? str ) {
5 | if ( exists parsing = str ) {
6 | if ( is Integer ret = Integer.parse( parsing ) ) {
7 | return ret;
8 | }
9 | else {
10 | return null;
11 | }
12 | }
13 | else {
14 | return null;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/dateTimeFromJSON.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.json {
2 | JsonObject
3 | }
4 | import ceylon.time {
5 | dateTime,
6 | DateTime
7 | }
8 | import herd.schedule.chime.cron {
9 | calendar
10 | }
11 |
12 |
13 | "Reads date-time from JSON into `DateTime`."
14 | since("0.2.0") by("Lis")
15 | DateTime dateTimeFromJSON(JsonObject dateTimeDescr)
16 | => dateTime (
17 | dateTimeDescr.getInteger(Chime.date.year), calendar.monthFromJson(dateTimeDescr, Chime.date.month),
18 | dateTimeDescr.getInteger(Chime.date.dayOfMonth), dateTimeDescr.getInteger(Chime.date.hours),
19 | dateTimeDescr.getInteger(Chime.date.minutes), dateTimeDescr.getInteger(Chime.date.seconds)
20 | );
21 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/every.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.json {
2 |
3 | JsonObject
4 | }
5 |
6 |
7 | "Builds `JsonObject` description of an interval timer."
8 | tagged( "Builder" )
9 | see( `class CronBuilder`, `class UnionBuilder` )
10 | since( "0.2.1" ) by( "Lis" )
11 | shared JsonObject every (
12 | "Timer interval measured in `timeUnit`." Integer interval,
13 | "Unit to measure `interval`." TimeUnit timeUnit = TimeUnit.seconds
14 | ) {
15 | "Timer interval has to be positive, while given is ``interval``."
16 | assert( interval > 0 );
17 |
18 | JsonObject ret= JsonObject {
19 | Chime.key.type -> Chime.type.interval,
20 | Chime.key.delay -> interval * timeUnit.secondsIn
21 | };
22 | return ret;
23 | }
24 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/extractors.ceylon:
--------------------------------------------------------------------------------
1 | import herd.schedule.chime.service {
2 | ChimeServices
3 | }
4 | import herd.schedule.chime.service.message {
5 | MessageSource
6 | }
7 | import herd.schedule.chime.service.timezone {
8 | TimeZone
9 | }
10 | import herd.schedule.chime.service.producer {
11 | EventProducer
12 | }
13 | import herd.schedule.chime.service.calendar {
14 | Calendar
15 | }
16 | import ceylon.json {
17 | JsonObject
18 | }
19 |
20 |
21 | "Extract services (time zone and message source) from timer or scheduler request."
22 | since("0.3.0") by("Lis")
23 | TimeServices|String> servicesFromRequest (
24 | "Timer description to get time zone name." JsonObject request,
25 | "Services Chime provides." ChimeServices services,
26 | "Default time services." TimeServices defaultServices
27 | ) {
28 | value converter = timeZoneFromRequest(request, services, defaultServices.timeZone);
29 | if (is TimeZone converter) {
30 | value messageSource = messageSourceFromRequest(request, services, defaultServices.messageSource);
31 | if (is MessageSource messageSource) {
32 | value eventProducer = eventProducerFromRequest(request, services, defaultServices.eventProducer);
33 | if (is EventProducer eventProducer) {
34 | value calendar = calendarFromRequest(request, services, defaultServices.calendar);
35 | if (is CalendarService calendar) {
36 | return TimeServices (
37 | converter, messageSource, eventProducer, calendar
38 | );
39 | }
40 | else {
41 | return calendar;
42 | }
43 | }
44 | else {
45 | return eventProducer;
46 | }
47 | }
48 | else {
49 | return messageSource;
50 | }
51 | }
52 | else {
53 | return converter;
54 | }
55 |
56 | }
57 |
58 |
59 | "Extracts time zone from timer or scheduler request."
60 | since("0.3.0") by("Lis")
61 | TimeZone|String> timeZoneFromRequest (
62 | "Timer description to get time zone name." JsonObject request,
63 | "Services Chime provides." ChimeServices services,
64 | "Default time zone applied if no time zone name is given." TimeZone defaultTimeZone
65 | ) {
66 | if (is String providerType = request[Chime.timeZoneProvider.key]) {
67 | return services.createTimeZone(providerType, request);
68 | }
69 | else if (is String timeZone = request[Chime.key.timeZone]) {
70 | return services.createTimeZone(Chime.timeZoneProvider.jvm, request);
71 | }
72 | else {
73 | return defaultTimeZone;
74 | }
75 | }
76 |
77 |
78 | "Extracts message source from timer or scheduler request."
79 | since("0.3.0") by("Lis")
80 | MessageSource|String> messageSourceFromRequest (
81 | "Timer description to get time zone name." JsonObject request,
82 | "Services Chime provides." ChimeServices services,
83 | "Default time zone applied if no time zone name is given." MessageSource defaultMessageSource
84 | ) {
85 | if (is String providerType = request[Chime.messageSource.key]) {
86 | return services.createMessageSource (
87 | providerType,
88 | request.getObjectOrNull(Chime.key.messageSourceOptions) else JsonObject{}
89 | );
90 | }
91 | else {
92 | return defaultMessageSource;
93 | }
94 | }
95 |
96 |
97 | "Extracts Event producer from timer or scheduler request."
98 | since("0.3.0") by("Lis")
99 | EventProducer|String> eventProducerFromRequest (
100 | "Timer description to get time zone name." JsonObject request,
101 | "Services Chime provides." ChimeServices services,
102 | "Default event producer applied if no one given at timer create request."
103 | EventProducer defaultProducer
104 | ) {
105 | if (is String providerType = request[Chime.eventProducer.key]) {
106 | return services.createEventProducer (
107 | providerType,
108 | request.getObjectOrNull(Chime.key.eventProducerOptions) else JsonObject{}
109 | );
110 | }
111 | else {
112 | if (exists opts = request.getObjectOrNull(Chime.key.eventProducerOptions)) {
113 | return services.createEventProducer(Chime.eventProducer.eventBus, opts);
114 | }
115 | else {
116 | return defaultProducer;
117 | }
118 | }
119 | }
120 |
121 |
122 | "Extracts Calendar from timer or scheduler request."
123 | since("0.3.0") by("Lis")
124 | CalendarService|String> calendarFromRequest (
125 | "Timer description to get time zone name." JsonObject request,
126 | "Services Chime provides." ChimeServices services,
127 | "Default calendar applied if no one given at timer create request."
128 | CalendarService defaultCalendar
129 | ) {
130 | if (is JsonObject calendarRequest = request[Chime.calendar.key]) {
131 | value cl = services.createCalendar(calendarRequest);
132 | if (is Calendar cl) {
133 | return CalendarServiceImpl (
134 | if (is Boolean ignore = calendarRequest[Chime.calendar.ignoreEvent]) then ignore else true,
135 | cl
136 | );
137 | }
138 | else {
139 | return cl;
140 | }
141 | }
142 | else {
143 | return defaultCalendar;
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/package.ceylon:
--------------------------------------------------------------------------------
1 | "Chime."
2 | since( "0.1.0" ) by( "Lis" )
3 | shared package herd.schedule.chime;
4 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/schedulerTopLevel.ceylon:
--------------------------------------------------------------------------------
1 | import io.vertx.ceylon.core.eventbus {
2 | EventBus,
3 | DeliveryOptions
4 | }
5 | import ceylon.json {
6 | JsonObject,
7 | JsonArray,
8 | ObjectValue
9 | }
10 |
11 |
12 | "Connects to already existed scheduler."
13 | see(`interface Scheduler`)
14 | throws(`class AssertionError`, "scheduler name contains ':'")
15 | tagged("Proxy")
16 | since("0.2.0") by("Lis")
17 | shared void connectScheduler (
18 | "Handler to receive created scheduler or error if occured."
19 | Anything(Throwable|Scheduler) handler,
20 | "Address to call _Chime_." String shimeAddress,
21 | "Event bus to send message to _Chime_." EventBus eventBus,
22 | "Name of the scheduler to be connected or created.
23 | Must not contain ':' symbol, since it separates scheduler and timer names."
24 | String name,
25 | "Timeout to send message with."
26 | Integer? sendTimeout = null
27 | ) {
28 | "Scheduler name must not contain ``Chime.configuration.nameSeparator``, since it separates scheduler and timer names."
29 | assert(!name.contains(Chime.configuration.nameSeparator));
30 | JsonObject request = JsonObject {
31 | Chime.key.operation -> Chime.operation.info,
32 | Chime.key.name -> name
33 | };
34 | if (exists sendTimeout) {
35 | eventBus.send (
36 | shimeAddress, request, DeliveryOptions(null, null, sendTimeout),
37 | SchedulerImpl.createSchedulerImpl(handler, eventBus, sendTimeout)
38 | );
39 | }
40 | else {
41 | eventBus.send (
42 | shimeAddress, request,
43 | SchedulerImpl.createSchedulerImpl(handler, eventBus, sendTimeout)
44 | );
45 | }
46 |
47 | }
48 |
49 |
50 | "Creates new scheduler or connects to already existed scheduler."
51 | see(`interface Scheduler`)
52 | throws(`class AssertionError`, "scheduler name contains ':'")
53 | tagged("Proxy")
54 | since("0.2.0") by("Lis")
55 | shared void createScheduler (
56 | "Handler to receive created scheduler or error if occured."
57 | Anything(Throwable|Scheduler) handler,
58 | "Address to call _Chime_." String shimeAddress,
59 | "Event bus to send message to _Chime_." EventBus eventBus,
60 | "Name of the scheduler to be connected or created.
61 | Must not contain ':' symbol, since it separates scheduler and timer names."
62 | String name,
63 | "`True` if new scheduler is paused and `false` if running.
64 | If scheduler has been created early its state is not changed."
65 | Boolean paused = false,
66 | "Optional time zone default for the scheduler."
67 | String? timeZone = null,
68 | "Optional time zone provider, default is \"jvm\"."
69 | String? timeZoneProvider = null,
70 | "Optional message source type default for the scheduler."
71 | String? messageSource = null,
72 | "Optional configuration passed to message source factory."
73 | ObjectValue? messageSourceConfig = null,
74 | "Event producer provider."
75 | String? eventProducer = null,
76 | "Optional configuration passed to event producer factory."
77 | JsonObject? eventProducerOptions = null,
78 | "Timeout to send message with."
79 | Integer? sendTimeout = null
80 | ) {
81 | "Scheduler name must not contain ``Chime.configuration.nameSeparator``, since it separates scheduler and timer names."
82 | assert(!name.contains(Chime.configuration.nameSeparator));
83 | JsonObject request = JsonObject {
84 | Chime.key.operation -> Chime.operation.create,
85 | Chime.key.name -> name,
86 | Chime.key.state -> (if (paused) then Chime.state.paused else Chime.state.running)
87 | };
88 | if (exists timeZone) {
89 | request.put(Chime.key.timeZone, timeZone);
90 | if (exists timeZoneProvider) {
91 | request.put( Chime.key.timeZoneProvider, timeZoneProvider );
92 | }
93 | }
94 | if (exists messageSource) {
95 | request.put(Chime.key.messageSource, messageSource);
96 | if (exists messageSourceConfig) {
97 | request.put(Chime.key.messageSourceOptions, messageSourceConfig);
98 | }
99 | }
100 | if (exists eventProducer) {
101 | request.put(Chime.key.eventProducer, eventProducer);
102 | }
103 | if (exists eventProducerOptions) {
104 | request.put(Chime.key.eventProducerOptions, eventProducerOptions);
105 | }
106 |
107 | if (exists sendTimeout) {
108 | eventBus.send (
109 | shimeAddress, request, DeliveryOptions(null, null, sendTimeout),
110 | SchedulerImpl.createSchedulerImpl(handler, eventBus, sendTimeout)
111 | );
112 | }
113 | else {
114 | eventBus.send (
115 | shimeAddress, request, SchedulerImpl.createSchedulerImpl(handler, eventBus, sendTimeout)
116 | );
117 | }
118 | }
119 |
120 |
121 | "Returns info on the given schedulers."
122 | tagged("Proxy")
123 | see(`function Scheduler.timersInfo`, `function Scheduler.info`)
124 | since("0.2.0") by("Lis")
125 | shared void schedulerInfo (
126 | "Handler to receive scheduler infos or error if occured."
127 | Anything(Throwable|SchedulerInfo[]) handler,
128 | "Address to call _Chime_."
129 | String shimeAddress,
130 | "Event bus to send message to _Chime_."
131 | EventBus eventBus,
132 | "List of scheduler name, the info to be requested for.
133 | If empty then info on all schedulers are requested."
134 | {String*} names = {},
135 | "Timeout to send message with."
136 | Integer? sendTimeout = null
137 |
138 | ) {
139 | JsonObject request = JsonObject {
140 | Chime.key.operation -> Chime.operation.info
141 | };
142 | if (!names.empty) {
143 | request.put(Chime.key.name, JsonArray(names));
144 | }
145 |
146 | if ( exists sendTimeout ) {
147 | eventBus.send (
148 | shimeAddress, request, DeliveryOptions(null, null, sendTimeout),
149 | SchedulerImpl.replyWithInfo(handler)
150 | );
151 | }
152 | else {
153 | eventBus.send (
154 | shimeAddress, request, SchedulerImpl.replyWithInfo(handler)
155 | );
156 | }
157 | }
158 |
159 |
160 | "Deletes schedulers or timers with the given names."
161 | tagged("Proxy")
162 | see(`function Scheduler.deleteTimers`, `function Scheduler.delete`)
163 | since("0.2.1") by("Lis")
164 | shared void delete (
165 | "Address to call _Chime_."
166 | String shimeAddress,
167 | "Event bus to send message to _Chime_."
168 | EventBus eventBus,
169 | "List of scheduler or timer names.
170 | If empty then every scheduler and timer have to be deleted."
171 | {String*} names = {},
172 | "Optional handler called with a list of names of actually deleted schedulers or timers."
173 | Anything(Throwable|{String*})? handler = null,
174 | "Timeout to send message with."
175 | Integer? sendTimeout = null
176 | ) {
177 | JsonObject request = JsonObject {
178 | Chime.key.operation -> Chime.operation.delete,
179 | Chime.key.name -> (if (names.empty) then "" else JsonArray(names))
180 | };
181 | if (exists handler) {
182 | if (exists sendTimeout) {
183 | eventBus.send (
184 | shimeAddress, request, DeliveryOptions(null, null, sendTimeout),
185 | SchedulerImpl.replyWithList(handler, Chime.key.schedulers)
186 | );
187 | }
188 | else {
189 | eventBus.send (
190 | shimeAddress, request, SchedulerImpl.replyWithList(handler, Chime.key.schedulers)
191 | );
192 | }
193 | }
194 | else {
195 | if (exists sendTimeout) {
196 | eventBus.send(shimeAddress, request, DeliveryOptions(null, null, sendTimeout));
197 | }
198 | else {
199 | eventBus.send(shimeAddress, request);
200 | }
201 | }
202 | }
203 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/ChimeServices.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.json {
2 | JsonObject
3 | }
4 | import io.vertx.ceylon.core {
5 |
6 | Vertx
7 | }
8 | import herd.schedule.chime.service.timer {
9 | TimeRow,
10 | TimeRowFactory
11 | }
12 | import herd.schedule.chime.service.timezone {
13 | TimeZone
14 | }
15 | import herd.schedule.chime.service.message {
16 | MessageSource
17 | }
18 | import herd.schedule.chime.service.producer {
19 | EventProducer
20 | }
21 | import herd.schedule.chime {
22 | Chime
23 | }
24 | import herd.schedule.chime.service.calendar {
25 | Calendar
26 | }
27 |
28 |
29 | "Provides Chime services:
30 | * creating [[TimeRow]] by the given timer description
31 | * creating [[TimeZone]] with the given provider and the given time zone
32 | * creating [[MessageSource]] with the given provider and the given details of the source
33 | "
34 | see(`interface TimeRowFactory`)
35 | since("0.3.0") by("Lis")
36 | shared interface ChimeServices
37 | {
38 |
39 | shared formal Service|String> createService (
40 | "Type of the service provider." String providerType,
41 | "Options passed to service provider." JsonObject options
42 | );
43 |
44 | "Creates time row by timer description. See about description in [[module herd.schedule.chime]]."
45 | shared default TimeRow|String> createTimeRow("Timer description." JsonObject description) {
46 | if (is String type = description[Chime.key.type]) {
47 | return createService(type, description);
48 | }
49 | else {
50 | return Chime.errors.codeTimerTypeHasToBeSpecified->Chime.errors.timerTypeHasToBeSpecified;
51 | }
52 | }
53 |
54 | "Creates calendar by description. See about description in [[module herd.schedule.chime]]."
55 | shared default Calendar|String> createCalendar("Calendar description." JsonObject description) {
56 | if (is String type = description[Chime.calendar.type]) {
57 | return createService(type, description);
58 | }
59 | else {
60 | return Chime.errors.codeCalendarTypeHasToBeSpecified->Chime.errors.calendarTypeHasToBeSpecified;
61 | }
62 | }
63 |
64 | "Creates time zone with given provider and for the given time zone name."
65 | shared default TimeZone|String> createTimeZone (
66 | "Type of the time zone provider." String providerType,
67 | "Time zone options." JsonObject options
68 | ) => createService(providerType, options);
69 |
70 | "Creates new message source."
71 | shared default MessageSource|String> createMessageSource (
72 | "Type of the message source provider." String providerType,
73 | "Message source options." JsonObject options
74 | ) => createService(providerType, options);
75 |
76 | "Creates new event producer."
77 | shared default EventProducer|String> createEventProducer (
78 | "Type of the event producer provider." String providerType,
79 | "Event producer options." JsonObject options
80 | ) => createService(providerType, options);
81 |
82 | "Time zone local to running machine."
83 | shared formal TimeZone localTimeZone;
84 |
85 | "Vertx instance the _Chime_ is running within."
86 | shared formal Vertx vertx;
87 |
88 | "Event bus address the _Chime_ listens to."
89 | shared formal String address;
90 | }
91 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/Extension.ceylon:
--------------------------------------------------------------------------------
1 | import io.vertx.ceylon.core {
2 | Vertx
3 | }
4 | import ceylon.json {
5 |
6 | JsonObject
7 | }
8 | import ceylon.language.meta.model {
9 | Type
10 | }
11 |
12 |
13 | "Extension (given as service provider) which acts as factory for some _Chime_ structural elements."
14 | since("0.3.0") by("Lis")
15 | shared interface Extension
16 | {
17 | "Type of service the extension provides."
18 | shared formal String type;
19 |
20 | "Type parameter the extension provides."
21 | shared Type parameter => `Element`;
22 |
23 | "Initializes the extension.
24 | Has to call `complete` when initialization is completed.
25 | By default immediately calls `complete`."
26 | shared default void initialize (
27 | "Vertx instance the _Chime_ is starting within."
28 | Vertx vertx,
29 | "Configuration the _Chime_ is starting with."
30 | JsonObject config,
31 | "Handler which has to be called when the extension initialization is completed.
32 | The handler takes extension to be added to the _Chime_
33 | or an error occured during initialization."
34 | Anything(Extension|Throwable) complete
35 | ) => complete(this);
36 |
37 | "Creates new structural element."
38 | shared formal Element|String> create (
39 | "Provides Chime services." ChimeServices services,
40 | "Options applied to the factory." JsonObject options
41 | );
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/calendar/Annually.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.time {
2 | DateTime,
3 | dateTime
4 | }
5 | import ceylon.time.base {
6 | Month,
7 | monthOf
8 | }
9 | import herd.schedule.chime.service {
10 | Extension,
11 | ChimeServices
12 | }
13 | import ceylon.json {
14 | JsonObject,
15 | JsonArray
16 | }
17 | import herd.schedule.chime {
18 | Chime
19 | }
20 | import herd.schedule.chime.cron {
21 | calendar
22 | }
23 |
24 |
25 | "Restricts time to the given months."
26 | since("0.3.0") by("Lis")
27 | class Annually(Set months) satisfies Calendar
28 | {
29 | shared actual Boolean inside(DateTime date) => date.month in months;
30 |
31 | shared actual DateTime nextOutside(DateTime date) {
32 | if (inside(date)) {
33 | variable DateTime ret = date.plusMonths(1);
34 | ret = dateTime(ret.year, ret.month, 1, ret.hours, ret.minutes, ret.seconds, 0);
35 | while (inside(ret)) {
36 | ret = ret.plusMonths(1);
37 | }
38 | return ret;
39 | }
40 | else {
41 | return date;
42 | }
43 | }
44 | }
45 |
46 |
47 | "Service provider of annually calendar.
48 | I.e. calendar which excludes a given list of months.
49 | To apply this calendar to the given timer / scheduler add to the JSON create request with following `JsonObject`:
50 | \"calendar\" -> JsonObject {
51 | \"type\" -> \"annually\";
52 | // monthes to be excluded from the fire event
53 | // digital, full or short (3 letters) string names are admitted
54 | \"months\" -> JsonArray{1, February, Mar}
55 | };
56 | "
57 | service(`interface Extension`)
58 | since("0.3.0") by("Lis")
59 | shared class AnnuallyFactory() satisfies CalendarFactory
60 | {
61 | shared actual Calendar|String> create(ChimeServices services, JsonObject options) {
62 | if (is JsonArray months = options[Chime.date.months]) {
63 | try {
64 | return Annually(set{for (item in months.narrow()) monthOf(calendar.digitalMonth(item))});
65 | }
66 | catch (AssertionError err) {
67 | return Chime.errors.assertionErrorCode -> err.message;
68 | }
69 | }
70 | else {
71 | return Chime.errors.codeAnnuallyCalendarMonths-> Chime.errors.annuallyCalendarMonths;
72 | }
73 | }
74 |
75 | shared actual String type => Chime.calendar.annually;
76 |
77 | }
78 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/calendar/Calendar.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.time {
2 | DateTime
3 | }
4 |
5 |
6 | "Restricts timer fire date - time."
7 | since("0.3.0") by("Lis")
8 | shared interface Calendar
9 | {
10 | "`true` if given date - time is inside the calendar and `false` if outside."
11 | shared formal Boolean inside(DateTime date);
12 |
13 | "Returns next date - time outside the calendar."
14 | shared formal DateTime nextOutside(DateTime date);
15 | }
16 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/calendar/CalendarFactory.ceylon:
--------------------------------------------------------------------------------
1 | import herd.schedule.chime.service {
2 | ChimeServices,
3 | Extension
4 | }
5 | import ceylon.json {
6 | JsonObject
7 | }
8 |
9 |
10 | "Calendar provider - creates [[Calendar]]."
11 | since("0.3.0") by("Lis")
12 | shared interface CalendarFactory satisfies Extension
13 | {
14 | "Creates new calendar with the given description.
15 | Returns created [[Calendar]] or error code -> message pair if some error occured."
16 | shared actual formal Calendar|String> create (
17 | "Provides Chime services." ChimeServices services,
18 | "Options with \"calendar\" description." JsonObject options
19 | );
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/calendar/Daily.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.time {
2 | Time,
3 | Period,
4 | DateTime,
5 | time
6 | }
7 | import herd.schedule.chime.service {
8 | Extension,
9 | ChimeServices
10 | }
11 | import ceylon.json {
12 | JsonObject
13 | }
14 | import herd.schedule.chime {
15 | Chime
16 | }
17 |
18 |
19 | "Resticts timer fire to to the given time range."
20 | since("0.3.0") by("Lis")
21 | class Daily(Time from, Time to, Period tolerancePeriod) satisfies Calendar
22 | {
23 | Time shifted = to.plus(tolerancePeriod);
24 |
25 | shared actual Boolean inside(DateTime date) {
26 | value t = date.time;
27 | return t >= from && t <= to;
28 | }
29 |
30 | shared actual DateTime nextOutside(DateTime date) {
31 | if (inside(date)) {
32 | return date.date.at(shifted);
33 | }
34 | else {
35 | return date;
36 | }
37 | }
38 | }
39 |
40 |
41 | "Service provider of daily calendar.
42 | I.e. calendar which excludes _from - to_ time of day.
43 | To apply this calendar to the given timer / scheduler add to the JSON create request with following `JsonObject`:
44 | \"calendar\" -> JsonObject {
45 | \"type\" -> \"daily\";
46 | // start time to be excluded
47 | \"from\" -> JsonObject {
48 | \"hours\" -> XXX; // default is 0
49 | \"minutes\" -> XXX; // default is 0
50 | \"seconds\" -> XXX; // default is 0
51 | };
52 | // end time to be excluded
53 | \"to\" -> JsonObject {
54 | \"hours\" -> XXX; // default is 0
55 | \"minutes\" -> XXX; // default is 0
56 | \"seconds\" -> XXX; // default is 0
57 | };
58 | // seconds to be added to the 'to' time when next out of calendar date is searched, default is 1s
59 | \"tolerance\" -> XXX;
60 | };
61 | "
62 | service(`interface Extension`)
63 | since("0.3.0") by("Lis")
64 | shared class DailyFactory() satisfies CalendarFactory
65 | {
66 |
67 | Time timeFromJson(JsonObject descr)
68 | => time (
69 | if (is Integer t = descr[Chime.date.hours]) then t else 0,
70 | if (is Integer t = descr[Chime.date.minutes]) then t else 0,
71 | if (is Integer t = descr[Chime.date.seconds]) then t else 0
72 | );
73 |
74 | shared actual Calendar|String> create(ChimeServices services, JsonObject options) {
75 | if (is JsonObject fromDescr = options[Chime.calendar.from],
76 | is JsonObject toDescr = options[Chime.calendar.to]
77 | ) {
78 | Time from = timeFromJson(fromDescr);
79 | Time to = timeFromJson(toDescr);
80 | if (to > from) {
81 | return Daily (
82 | from, to,
83 | Period{seconds = if (is Integer t = options[Chime.calendar.tolerance]) then t else 1;}
84 | );
85 | }
86 | else {
87 | return Chime.errors.codeDailyCalendarOrder -> Chime.errors.dailyCalendarOrder;
88 | }
89 | }
90 | else {
91 | return Chime.errors.codeDailyCalendarFormat -> Chime.errors.dailyCalendarFormat;
92 | }
93 | }
94 |
95 | shared actual String type => Chime.calendar.daily;
96 |
97 | }
98 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/calendar/Dativity.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.time {
2 | DateTime
3 | }
4 | import herd.schedule.chime.service {
5 | ChimeServices,
6 | Extension
7 | }
8 | import ceylon.json {
9 | JsonObject,
10 | JsonArray
11 | }
12 | import herd.schedule.chime {
13 | Chime
14 | }
15 |
16 |
17 | "Restricts time to the given dates."
18 | since("0.3.0") by("Lis")
19 | class Dativity(Set dates) satisfies Calendar
20 | {
21 | shared actual Boolean inside(DateTime date) => DayMonth.fromDate(date.date) in dates;
22 |
23 | shared actual DateTime nextOutside(DateTime date) {
24 | variable DateTime ret = date;
25 | while (inside(ret)) {
26 | ret = ret.plusDays(1);
27 | }
28 | return ret;
29 | }
30 | }
31 |
32 |
33 | "Service provider of dativity calendar.
34 | I.e. calendar which excludes a given list of dates.
35 | To apply this calendar to the given timer / scheduler add to the JSON create request with following `JsonObject`:
36 | \"calendar\" -> JsonObject {
37 | \"type\" -> \"dativity\";
38 | // List of dates to be excluded from the fire event.
39 | // Each date is JsonObject which contains day, month and optionally year.
40 | // If year is omitted the date is applied to any year
41 | // If only a one date is given it can be specified without JsonArray, i.e. JsonObject can be stored under dates key
42 | \"dates\" -> JsonArray {
43 | JsonObject {
44 | \"day of month\" -> Integer day
45 | \"month\" -> Integer or String month
46 | \"year\" -> Integer year, optional
47 | }
48 | }
49 | };
50 | "
51 | service(`interface Extension`)
52 | since("0.3.0") by("Lis")
53 | shared class DativityFactory() satisfies CalendarFactory
54 | {
55 | shared actual Calendar|String> create(ChimeServices services, JsonObject options) {
56 | value dativity = options[Chime.calendar.dates];
57 | if (is JsonArray dativity) {
58 | try {
59 | return Dativity(set{for(item in dativity.narrow()) DayMonth.fromJson(item)});
60 |
61 | }
62 | catch (AssertionError err) {
63 | return Chime.errors.assertionErrorCode -> err.message;
64 | }
65 | }
66 | else if (is JsonObject dativity) {
67 | try {
68 | return Dativity(set{DayMonth.fromJson(dativity)});
69 |
70 | }
71 | catch (AssertionError err) {
72 | return Chime.errors.assertionErrorCode -> err.message;
73 | }
74 | }
75 | else {
76 | return Chime.errors.codeDativityCalendarDates-> Chime.errors.dativityCalendarDates;
77 | }
78 | }
79 |
80 | shared actual String type => Chime.calendar.dativity;
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/calendar/DayMonth.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.time.base {
2 | Month,
3 | monthOf
4 | }
5 | import ceylon.time {
6 | Date
7 | }
8 | import ceylon.json {
9 | JsonObject
10 | }
11 | import herd.schedule.chime {
12 | Chime
13 | }
14 | import herd.schedule.chime.cron {
15 | calendar
16 | }
17 |
18 |
19 | "Contains day, month and optional year."
20 | since("0.3.0") by("Lis")
21 | class DayMonth extends Object {
22 |
23 | shared static DayMonth fromJson(JsonObject descr) {
24 | "Year has to be Integer or undefined."
25 | assert (is Integer? y = descr[Chime.date.year]);
26 | return DayMonth.with(descr.getInteger(Chime.date.dayOfMonth), calendar.monthFromJson(descr, Chime.date.month), y);
27 | }
28 |
29 |
30 | Integer day;
31 | Month month;
32 | Integer? year;
33 |
34 | shared new fromDate(Date date) extends Object() {
35 | this.day = date.day;
36 | this.month = date.month;
37 | this.year = date.year;
38 | }
39 |
40 | shared new with(Integer day, Month|Integer month, Integer? year = null) extends Object() {
41 | this.day = day;
42 | this.month = monthOf(month);
43 | this.year = year;
44 | }
45 |
46 | shared actual Boolean equals(Object obj) {
47 | if (is DayMonth obj) {
48 | if (day == obj.day && month == obj.month) {
49 | if (exists y = year, exists objy = obj.year) {
50 | return y == objy;
51 | }
52 | else {
53 | return true;
54 | }
55 | }
56 | else {
57 | return false;
58 | }
59 | }
60 | else {
61 | return false;
62 | }
63 | }
64 |
65 | shared actual Integer hash {
66 | variable value hash = 1;
67 | hash = 31*hash + day;
68 | hash = 31*hash + month.hash;
69 | return hash;
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/calendar/IntersectionCalendar.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.time {
2 | DateTime
3 | }
4 | import herd.schedule.chime.service {
5 | ChimeServices,
6 | Extension
7 | }
8 | import ceylon.json {
9 | JsonObject
10 | }
11 | import herd.schedule.chime {
12 | Chime
13 | }
14 |
15 |
16 | "Intersects calendars with logical `and`."
17 | since("0.3.0") by("Lis")
18 | class IntersectionCalendar({Calendar*} calendars) satisfies Calendar
19 | {
20 | shared actual Boolean inside(DateTime date) {
21 | for (item in calendars) {
22 | if (!item.inside(date)) {
23 | return false;
24 | }
25 | }
26 | return true;
27 | }
28 |
29 | shared actual DateTime nextOutside(DateTime date) {
30 | variable DateTime ret = date;
31 | while (inside(ret)) {
32 | variable DateTime? min = null;
33 | for (item in calendars) {
34 | if (item.inside(ret)) {
35 | value f = item.nextOutside(ret);
36 | if (exists ff = min) {
37 | if (f < ff) {min = f;}
38 | }
39 | else {min = f;}
40 | }
41 | }
42 | if (exists m = min) {ret = m;}
43 | else {break;}
44 | }
45 | return ret;
46 | }
47 |
48 | }
49 |
50 |
51 | "Service provider of intersection calendar.
52 | I.e. calendar which intersects a given list of calendars.
53 | To apply this calendar to the given timer / scheduler add to the JSON create request with following `JsonObject`:
54 | \"calendar\" -> JsonObject {
55 | \"type\" -> \"intersection\";
56 | // List of calendars to be intersected.
57 | \"calendars\" -> JsonArray {
58 | // calendars in `JsonObject`'s
59 | }
60 | };
61 | "
62 | service(`interface Extension`)
63 | since("0.3.0") by("Lis")
64 | shared class IntersectionFactory() satisfies CalendarFactory
65 | {
66 | shared actual Calendar|String> create(ChimeServices services, JsonObject options) {
67 | value calendars = extractCalendars(services, options);
68 | if (is {Calendar*} calendars) {
69 | return IntersectionCalendar(calendars);
70 | }
71 | else {
72 | return calendars;
73 | }
74 | }
75 |
76 | shared actual String type => Chime.calendar.intersection;
77 |
78 | }
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/calendar/LastDayOfWeek.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.time.base {
2 | DayOfWeek
3 | }
4 | import ceylon.time {
5 | DateTime
6 | }
7 | import herd.schedule.chime.service {
8 | Extension,
9 | ChimeServices
10 | }
11 | import ceylon.json {
12 | JsonObject
13 | }
14 | import herd.schedule.chime {
15 | Chime
16 | }
17 | import herd.schedule.chime.cron {
18 | calendar
19 | }
20 |
21 |
22 | "Restricts time to the last day of week of the month."
23 | since("0.3.0") by("Lis")
24 | class LastDayOfWeek(DayOfWeek dw) satisfies Calendar
25 | {
26 | shared actual Boolean inside(DateTime date) => date.dayOfWeek == dw && date.plusWeeks(1).month != date.month;
27 |
28 | shared actual DateTime nextOutside(DateTime date) {
29 | if (inside(date)) {
30 | return date.plusDays(1);
31 | }
32 | else {
33 | return date;
34 | }
35 | }
36 | }
37 |
38 |
39 | "Service provider of last day of week calendar.
40 | I.e. calendar which excludes only a last day of week in each month.
41 | To apply this calendar to the given timer / scheduler add to the JSON create request with following `JsonObject`:
42 | \"calendar\" -> JsonObject {
43 | \"type\" -> \"last day of week\";
44 | \"day of week\" -> Integer|String // day of week number or name (Sunday has number 1)
45 | };
46 | "
47 | service(`interface Extension`)
48 | since("0.3.0") by("Lis")
49 | shared class LastDayOfWeekFactory() satisfies CalendarFactory
50 | {
51 | shared actual Calendar|String> create(ChimeServices services, JsonObject options) {
52 | try {
53 | return LastDayOfWeek(calendar.dayOfWeekFromJson(options, Chime.date.dayOfWeek));
54 | }
55 | catch (AssertionError err) {
56 | return Chime.errors.assertionErrorCode -> err.message;
57 | }
58 | }
59 |
60 | shared actual String type => Chime.calendar.lastDayOfWeek;
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/calendar/Monthly.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.time {
2 | DateTime
3 | }
4 | import herd.schedule.chime.service {
5 | ChimeServices,
6 | Extension
7 | }
8 | import ceylon.json {
9 | JsonObject,
10 | JsonArray
11 | }
12 | import herd.schedule.chime {
13 | Chime
14 | }
15 |
16 |
17 | "Retricts time to the given days of month."
18 | since("0.3.0") by("Lis")
19 | class Monthly(Set restrictedDays) satisfies Calendar
20 | {
21 | shared actual Boolean inside(DateTime date) => date.day in restrictedDays;
22 |
23 | shared actual DateTime nextOutside(DateTime date) {
24 | variable DateTime ret = date;
25 | while (inside(ret)) {
26 | ret = ret.plusDays(1);
27 | }
28 | return ret;
29 | }
30 | }
31 |
32 | "Service provider of monthly calendar.
33 | I.e. calendar which excludes a given list of days for every month.
34 | To apply this calendar to the given timer / scheduler add to the JSON create request with following `JsonObject`:
35 | \"calendar\" -> JsonObject {
36 | \"type\" -> \"monthly\";
37 | \"days of month\" -> JsonArray{1, 2, 3} // days to be excluded from the fire event
38 | };
39 | "
40 | service(`interface Extension`)
41 | since("0.3.0") by("Lis")
42 | shared class MonthlyFactory() satisfies CalendarFactory
43 | {
44 | shared actual Calendar|String> create(ChimeServices services, JsonObject options) {
45 | if (is JsonArray days = options[Chime.date.daysOfMonth]) {
46 | return Monthly(set(days.narrow()));
47 | }
48 | else {
49 | return Chime.errors.codeMonthlyCalendarDaysOfMonth-> Chime.errors.monthlyCalendarDaysOfMonth;
50 | }
51 | }
52 |
53 | shared actual String type => Chime.calendar.monthly;
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/calendar/UnionCalendar.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.time {
2 | DateTime
3 | }
4 | import herd.schedule.chime.service {
5 | ChimeServices,
6 | Extension
7 | }
8 | import ceylon.json {
9 | JsonObject
10 | }
11 | import herd.schedule.chime {
12 | Chime
13 | }
14 |
15 |
16 | "Unions calendars with logical `or`."
17 | since("0.3.0") by("Lis")
18 | class UnionCalendar({Calendar*} calendars) satisfies Calendar
19 | {
20 | shared actual Boolean inside(DateTime date) {
21 | for (item in calendars) {
22 | if (item.inside(date)) {
23 | return true;
24 | }
25 | }
26 | return false;
27 | }
28 |
29 | shared actual DateTime nextOutside(DateTime date) {
30 | variable DateTime ret = date;
31 | while (inside(ret)) {
32 | variable DateTime max = ret;
33 | for (item in calendars) {
34 | if (item.inside(ret)) {
35 | value f = item.nextOutside(ret);
36 | if (f > max) {max = f;}
37 | }
38 | }
39 | ret = max;
40 | }
41 | return ret;
42 | }
43 |
44 | }
45 |
46 |
47 | "Service provider of union calendar.
48 | I.e. calendar which unons a given list of calendar.
49 | To apply this calendar to the given timer / scheduler add to the JSON create request with following `JsonObject`:
50 | \"calendar\" -> JsonObject {
51 | \"type\" -> \"union\";
52 | // List of calendars to be unioned.
53 | \"calendars\" -> JsonArray {
54 | // calendars in `JsonObject`'s
55 | }
56 | };
57 | "
58 | service(`interface Extension`)
59 | since("0.3.0") by("Lis")
60 | shared class UnionFactory() satisfies CalendarFactory
61 | {
62 |
63 | shared actual Calendar|String> create(ChimeServices services, JsonObject options) {
64 | value calendars = extractCalendars(services, options);
65 | if (is {Calendar*} calendars) {
66 | return UnionCalendar(calendars);
67 | }
68 | else {
69 | return calendars;
70 | }
71 | }
72 |
73 | shared actual String type => Chime.calendar.union;
74 |
75 | }
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/calendar/WeekdayOfMonth.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.time.base {
2 | DayOfWeek
3 | }
4 | import ceylon.time {
5 | DateTime
6 | }
7 | import herd.schedule.chime.service {
8 | ChimeServices,
9 | Extension
10 | }
11 | import herd.schedule.chime.cron {
12 | calendar
13 | }
14 | import ceylon.json {
15 | JsonObject
16 | }
17 | import herd.schedule.chime {
18 | Chime
19 | }
20 |
21 |
22 | "Restricts time to the given weekday of month."
23 | since("0.3.0") by("Lis")
24 | class WeekdayOfMonth(DayOfWeek dw, Integer order) satisfies Calendar
25 | {
26 | shared actual Boolean inside(DateTime date) => date.dayOfWeek == dw && order == (date.day - 1) / 7 + 1;
27 |
28 | shared actual DateTime nextOutside(DateTime date) {
29 | if (inside(date)) {
30 | return date.plusDays(1);
31 | }
32 | else {
33 | return date;
34 | }
35 | }
36 |
37 | }
38 |
39 |
40 | "Service provider of weekday of month calendar.
41 | I.e. calendar which excludes a given weekday with the given order in each month (i.e. excludes nth weekday).
42 | To apply this calendar to the given timer / scheduler add to the JSON create request with following `JsonObject`:
43 | \"calendar\" -> JsonObject {
44 | \"type\" -> \"weekday of month\";
45 | // day of week number or name (Sunday has number 1),
46 | // digital, full or short (3 letters) string names are admitted
47 | \"day of week\" -> Integer|String;
48 | \"order\" -> Integer // weekday order
49 | };
50 | "
51 | service(`interface Extension`)
52 | since("0.3.0") by("Lis")
53 | shared class WeekdayOfMonthFactory() satisfies CalendarFactory
54 | {
55 | shared actual Calendar|String> create(ChimeServices services, JsonObject options) {
56 | try {
57 | return WeekdayOfMonth (
58 | calendar.dayOfWeekFromJson(options, Chime.date.dayOfWeek),
59 | options.getInteger(Chime.date.order)
60 | );
61 | }
62 | catch (AssertionError err) {
63 | return Chime.errors.assertionErrorCode -> err.message;
64 | }
65 | }
66 |
67 | shared actual String type => Chime.calendar.weekdayOfMonth;
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/calendar/Weekly.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.time {
2 | DateTime
3 | }
4 | import ceylon.time.base {
5 | DayOfWeek
6 | }
7 | import herd.schedule.chime.service {
8 | ChimeServices,
9 | Extension
10 | }
11 | import herd.schedule.chime.cron {
12 | calendar
13 | }
14 | import ceylon.json {
15 | JsonObject,
16 | JsonArray
17 | }
18 | import herd.schedule.chime {
19 | Chime
20 | }
21 |
22 |
23 | "Restricts time to week days."
24 | since("0.3.0") by("Lis")
25 | class Weekly(Set restrictedDays) satisfies Calendar
26 | {
27 | shared actual Boolean inside(DateTime date) => date.dayOfWeek in restrictedDays;
28 |
29 | shared actual DateTime nextOutside(DateTime date) {
30 | variable DateTime ret = date;
31 | while (inside(ret)) {
32 | ret = ret.plusDays(1);
33 | }
34 | return ret;
35 | }
36 | }
37 |
38 |
39 | "Service provider of weekly calendar.
40 | I.e. calendar which excludes a given list of week days every week.
41 | To apply this calendar to the given timer / scheduler add to the JSON create request with following `JsonObject`:
42 | \"calendar\" -> JsonObject {
43 | \"type\" -> \"weekly\";
44 | // days of week to be excluded from the fire event, Sunday has index 1
45 | // digital, full or short (3 letters) string names are admitted
46 | \"days of week\" -> JsonArray{1, Monday, Tue}
47 | };
48 | "
49 | service(`interface Extension`)
50 | since("0.3.0") by("Lis")
51 | shared class WeeklyFactory() satisfies CalendarFactory
52 | {
53 | shared actual Calendar|String> create(ChimeServices services, JsonObject options) {
54 | if (is JsonArray dow = options[Chime.date.daysOfWeek]) {
55 | try {
56 | return Weekly(set{for (item in dow.narrow()) calendar.dayOfWeekFromString(item)});
57 | }
58 | catch (AssertionError err) {
59 | return Chime.errors.assertionErrorCode -> err.message;
60 | }
61 | }
62 | else {
63 | return Chime.errors.codeWeeklyCalendarDaysOfWeek-> Chime.errors.weeklyCalendarDaysOfWeek;
64 | }
65 | }
66 |
67 | shared actual String type => Chime.calendar.weekly;
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/calendar/extractCalendars.ceylon:
--------------------------------------------------------------------------------
1 | import herd.schedule.chime.service {
2 | ChimeServices
3 | }
4 | import ceylon.json {
5 | JsonObject,
6 | JsonArray
7 | }
8 | import ceylon.collection {
9 | ArrayList
10 | }
11 | import herd.schedule.chime {
12 | Chime
13 | }
14 |
15 |
16 | "Extracts a list of calendars from Json description."
17 | since("0.3.0") by("Lis")
18 | {Calendar*}|String> extractCalendars(ChimeServices services, JsonObject options) {
19 | if (is JsonArray calendarDescrs = options[Chime.calendar.calendars]) {
20 | ArrayList calendars = ArrayList();
21 | for(item in calendarDescrs.narrow()) {
22 | value c = services.createCalendar(item);
23 | if (is Calendar c) {
24 | calendars.add(c);
25 | }
26 | else {
27 | return c;
28 | }
29 | }
30 | return calendars;
31 | }
32 | else {
33 | return Chime.errors.codeCalendars-> Chime.errors.calendars;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/calendar/package.ceylon:
--------------------------------------------------------------------------------
1 | "Provides calendar services.
2 |
3 | Calendar restricts date/time a timer fires at.
4 | So, when calendar is added to timer,
5 | timer will not fire at dates/time calendar specifies.
6 | In order to add calendar to the timer put calendar description to timer
7 | create request under \"calendar\" key. See desciption details at each given calendar.
8 |
9 | If calendar is applied at scheduler level then each timer within the scheduler
10 | with unspecified calendar will get the cheduler level calendar.
11 |
12 | To build your own [[Calendar]] follow the steps:
13 | 1. Implement [[CalendarFactory]].
14 | 2. Mark the class from 1. with `service(`\` `interface Extension`\` `)`.
15 | 3. Deploy _Chime_ with configuration provided modules to serach the services:
16 | JsonObject {
17 | \"services\" -> [\"module name/module version\"]
18 | }
19 |
20 | "
21 | since("0.3.0") by("Lis")
22 | shared package herd.schedule.chime.service.calendar;
23 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/message/DirectMessageSourceFactory.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.json {
2 | JsonObject,
3 | ObjectValue
4 | }
5 | import herd.schedule.chime {
6 |
7 | Chime,
8 | TimerFire
9 | }
10 | import herd.schedule.chime.service {
11 | ChimeServices,
12 | Extension
13 | }
14 |
15 |
16 | "Factory to create message source which returns message given in timer create request.
17 | This is default message source factory."
18 | service(`interface Extension`)
19 | since("0.3.0") by("Lis")
20 | shared class DirectMessageSourceFactory satisfies MessageSourceFactory
21 | {
22 |
23 | "Default message source - applies `onMessage` with given event message."
24 | shared static object directMessageSource satisfies MessageSource {
25 | shared actual void extract(TimerFire event, Anything(ObjectValue?) onMessage)
26 | => onMessage(event.message);
27 | }
28 |
29 | "New `DirectMessageSourceFactory` instance."
30 | shared new () {}
31 |
32 |
33 | shared actual MessageSource|String> create(ChimeServices services, JsonObject config)
34 | => directMessageSource;
35 |
36 | shared actual String type => Chime.messageSource.direct;
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/message/MessageSource.ceylon:
--------------------------------------------------------------------------------
1 | import herd.schedule.chime {
2 | TimerFire
3 | }
4 | import ceylon.json {
5 | ObjectValue
6 | }
7 |
8 |
9 | "Extension which provides messages to be added to a timer fire event.
10 | Generally, a message source is instantiated by [[MessageSourceFactory]] given as service provider,
11 | see [[package herd.schedule.chime.service]].
12 |
13 | It is proposed that message attached to the timer create request contains some info for this source.
14 | The message source may use this info to extract provided message.
15 | "
16 | since("0.3.0") by("Lis")
17 | see(`interface MessageSourceFactory`)
18 | shared interface MessageSource
19 | {
20 | "Extracts message and message headers from this source.
21 | When message and message headers are ready call `onMessage` handler."
22 | shared formal void extract (
23 | "Event the message to be attached to.
24 | [[TimerFire.message]] is taken from timer or scheduler create request.
25 | In the sent event the message is to be replaced with one the given to `onMessage` handler."
26 | TimerFire event,
27 | "Handler which takes the message."
28 | Anything(ObjectValue?) onMessage
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/message/MessageSourceFactory.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.json {
2 | JsonObject
3 | }
4 | import herd.schedule.chime.service {
5 | Extension,
6 | ChimeServices
7 | }
8 |
9 |
10 | "Creates message source."
11 | since("0.3.0") by("Lis")
12 | shared interface MessageSourceFactory satisfies Extension
13 | {
14 | "Creates new message source."
15 | shared actual formal MessageSource|String> create (
16 | "Provides Chime services." ChimeServices services,
17 | "Message source configuration came with scheduler or timer create request." JsonObject config
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/message/package.ceylon:
--------------------------------------------------------------------------------
1 | "Provides message source services.
2 |
3 | A timer may fire with a message attached to the fire event. The message and message headers may be extracted from some source.
4 | [[MessageSource]] provides a way to extract messages. A service provider implementing [[MessageSourceFactory]]
5 | is responsible to instantiate particular [[MessageSource]].
6 |
7 | built-in message sources:
8 | * direct source, which attaches to the fire event a message given at timer create request,
9 | created by [[herd.schedule.chime.service.message::DirectMessageSourceFactory]].
10 |
11 | To build your own [[herd.schedule.chime.service.message::MessageSource]] follow the steps:
12 | 1. Implement [[herd.schedule.chime.service.message::MessageSourceFactory]].
13 | 2. Mark the class from 1. with `service(`\` `interface MessageSourceFactory`\` `)`.
14 | 3. Deploy _Chime_ with configuration provided modules to serach the services:
15 | JsonObject {
16 | \"services\" -> [\"module name/module version\"]
17 | }
18 |
19 | In timer or scheduler create request message source can be specified under \"message source\" key.
20 | Additional configuration passed to the factory may be given under \"message source configuration\" key.
21 |
22 | > At scheduler level default message source may be specified, which is applied to timer if no message source
23 | is given at timer create request.
24 | "
25 | since("0.3.0") by("Lis")
26 | shared package herd.schedule.chime.service.message;
27 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/package.ceylon:
--------------------------------------------------------------------------------
1 | "Chime extensions with service providers.
2 |
3 | > See also `ceylon.language.service` annotation.
4 |
5 | Chime searches service providers only in modules specified in verticle deployed configuration:
6 | JsonObject modulesWithProviders = JsonObject {
7 | \"services\" -> [
8 | \"module 1 name/module 1 version\",
9 | \"module nth name/module nth version\"
10 | ]
11 | };
12 | Chime().deploy (
13 | vertx.vertx(),
14 | DeploymentOptions(modulesWithProviders)
15 | );
16 |
17 |
18 | Each service provider has to satisfy [[Extension]] interface
19 | and be marked with `service(`\` `interface Extension`\` `)` annotation.
20 |
21 | Following services are available:
22 | * timer ([[package herd.schedule.chime.service.timer]]) - instantiating built-in or custom timers
23 | * time zone ([[package herd.schedule.chime.service.timezone]]) - extracting time zone
24 | * message source ([[package herd.schedule.chime.service.message]]) - extracting messages applied to timer fire event
25 | * calendar ([[package herd.schedule.chime.service.calendar]]) - creating calendars which can be applied to bound eventing date/time
26 | * event producer ([[package herd.schedule.chime.service.producer]]) - creating a producer applied to send timer event
27 |
28 | "
29 | since("0.3.0") by("Lis")
30 | shared package herd.schedule.chime.service;
31 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/producer/EBProducerFactory.ceylon:
--------------------------------------------------------------------------------
1 | import herd.schedule.chime.service {
2 | ChimeServices,
3 | Extension
4 | }
5 | import ceylon.json {
6 | JsonObject
7 | }
8 | import herd.schedule.chime {
9 | Chime
10 | }
11 | import io.vertx.ceylon.core.eventbus {
12 | EventBus,
13 | deliveryOptions
14 | }
15 |
16 |
17 | "Factory which creates [[EventBusProducer]]"
18 | service(`interface Extension`)
19 | since("0.3.0") by("Lis")
20 | shared class EBProducerFactory satisfies ProducerFactory
21 | {
22 |
23 | "Creates default producer, which operates via the given event bus and sends events (rather than publishes)."
24 | shared static EventProducer createDefaultProducer(EventBus eventBus) => EventBusProducer(eventBus, false, null);
25 |
26 | "New `ProducerFactory` instance."
27 | shared new () {}
28 |
29 | shared actual EventProducer|String> create(ChimeServices services, JsonObject options)
30 | => EventBusProducer (
31 | services.vertx.eventBus(),
32 | if (is Boolean b = options[Chime.eventProducer.publish]) then b else false,
33 | if (is JsonObject opts = options[Chime.eventProducer.deliveryOptions])
34 | then deliveryOptions.fromJson(opts) else null
35 | );
36 |
37 | shared actual String type => Chime.eventProducer.eventBus;
38 |
39 | shared actual String string => "EventBus producer factory.";
40 | }
41 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/producer/EventBusProducer.ceylon:
--------------------------------------------------------------------------------
1 | import io.vertx.ceylon.core.eventbus {
2 | EventBus,
3 | DeliveryOptions
4 | }
5 | import herd.schedule.chime {
6 | TimerEvent,
7 | TimerCompleted
8 | }
9 | import ceylon.json {
10 | JsonObject
11 | }
12 |
13 |
14 | "Sends timer events via event bus."
15 | since("0.3.0") by("Lis")
16 | class EventBusProducer (
17 | EventBus eventBus,
18 | Boolean publish,
19 | DeliveryOptions? options
20 | )
21 | satisfies EventProducer
22 | {
23 |
24 | void publishEvent(TimerEvent event) {
25 | if (exists opt = options) { eventBus.publish(event.timerName, convert(event), opt); }
26 | else { eventBus.publish(event.timerName, convert(event)); }
27 | }
28 |
29 | void sendEvent(TimerEvent event) {
30 | if (exists opt = options) { eventBus.send(event.timerName, convert(event), opt); }
31 | else { eventBus.send(event.timerName, convert(event)); }
32 | }
33 |
34 | "Converts timer event into `JsonObject` which to be send via event bus.
35 | By default applies [[TimerEvent.toJson]]."
36 | shared default JsonObject convert(TimerEvent event) => event.toJson();
37 |
38 | shared actual void send(TimerEvent event) {
39 | if (publish || event is TimerCompleted) {
40 | publishEvent(event);
41 | }
42 | else {
43 | sendEvent(event);
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/producer/EventProducer.ceylon:
--------------------------------------------------------------------------------
1 | import herd.schedule.chime {
2 | TimerEvent
3 | }
4 |
5 |
6 | "Produces timer events."
7 | since("0.3.0") by("Lis")
8 | shared interface EventProducer
9 | {
10 | "Sends timer event."
11 | shared formal void send(TimerEvent event);
12 | }
13 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/producer/ProducerFactory.ceylon:
--------------------------------------------------------------------------------
1 | import herd.schedule.chime.service {
2 | Extension,
3 | ChimeServices
4 | }
5 | import ceylon.json {
6 | JsonObject
7 | }
8 |
9 |
10 | "Factory to crshared eate [[EventProducer]]."
11 | since("0.3.0") by("Lis")
12 | shared interface ProducerFactory satisfies Extension
13 | {
14 | "Creates new event producer."
15 | shared actual formal EventProducer|String> create (
16 | "Provides Chime services." ChimeServices services,
17 | "Producer options." JsonObject options
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/producer/package.ceylon:
--------------------------------------------------------------------------------
1 | "Extensions which produces timer events."
2 | since("0.3.0") by("Lis")
3 | shared package herd.schedule.chime.service.producer;
4 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/timer/CronFactory.ceylon:
--------------------------------------------------------------------------------
1 | import herd.schedule.chime.cron {
2 | parseCron
3 | }
4 | import ceylon.json {
5 | JsonObject
6 | }
7 | import herd.schedule.chime {
8 | Chime
9 | }
10 | import herd.schedule.chime.service {
11 | ChimeServices,
12 | Extension
13 | }
14 |
15 |
16 | "Factory to create cron-style timers."
17 | service(`interface Extension`)
18 | since("0.3.0") by("Lis")
19 | shared class CronFactory() satisfies TimeRowFactory
20 | {
21 |
22 | Integer maxYearPeriod = 100;
23 |
24 | shared actual String type => Chime.type.cron;
25 |
26 | shared actual TimeRow|String> create(ChimeServices services, JsonObject description) {
27 | if (is String seconds = description[Chime.date.seconds],
28 | is String minutes = description[Chime.date.minutes],
29 | is String hours = description[Chime.date.hours],
30 | is String daysOfMonth = description[Chime.date.daysOfMonth],
31 | is String months = description[Chime.date.months]
32 | ) {
33 | // days of week - nonmandatory
34 | String? daysOfWeek;
35 | if (is String str = description[Chime.date.daysOfWeek]) {
36 | daysOfWeek = str;
37 | }
38 | else {
39 | daysOfWeek = null;
40 | }
41 |
42 | // years - nonmandatory
43 | String? years;
44 | if (is String str = description[Chime.date.years]) {
45 | years = str;
46 | }
47 | else {
48 | years = null;
49 | }
50 |
51 | if (exists cronExpr = parseCron(seconds, minutes, hours, daysOfMonth, months, daysOfWeek, years, maxYearPeriod)) {
52 | return TimeRowCronStyle( cronExpr );
53 | }
54 | else {
55 | return Chime.errors.codeIncorrectCronTimerDescription->Chime.errors.incorrectCronTimerDescription;
56 | }
57 |
58 | }
59 | else {
60 | return Chime.errors.codeIncorrectCronTimerDescription->Chime.errors.incorrectCronTimerDescription;
61 | }
62 | }
63 |
64 | shared actual String string => "cron time row factory";
65 |
66 | }
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/timer/IntervalFactory.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.json {
2 | JsonObject
3 | }
4 | import herd.schedule.chime {
5 | Chime
6 | }
7 | import herd.schedule.chime.service {
8 | ChimeServices,
9 | Extension
10 | }
11 |
12 |
13 | "Factory to create interval timers."
14 | service(`interface Extension`)
15 | since("0.3.0") by("Lis")
16 | shared class IntervalFactory() satisfies TimeRowFactory
17 | {
18 |
19 | shared actual String type => Chime.type.interval;
20 |
21 | shared actual TimeRow|String> create(ChimeServices services, JsonObject description) {
22 | if (is Integer delay = description[Chime.key.delay]) {
23 | if (delay > 0) {
24 | return TimeRowInterval(delay * 1000);
25 | }
26 | else {
27 | return Chime.errors.codeDelayHasToBeGreaterThanZero->Chime.errors.delayHasToBeGreaterThanZero;
28 | }
29 | }
30 | return Chime.errors.codeDelayHasToBeSpecified->Chime.errors.delayHasToBeSpecified;
31 | }
32 |
33 | shared actual String string => "interval time row factory";
34 |
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/timer/TimeRow.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.time {
2 |
3 | DateTime
4 | }
5 |
6 |
7 | "Time row interface. Acts like _enumerator_ but might be restarted from any date.
8 | Generally, a time row is instantiated by [[TimeRowFactory]] given as service provider,
9 | see [[package herd.schedule.chime.service]]."
10 | see(`interface TimeRowFactory`)
11 | since("0.1.0") by("Lis")
12 | shared interface TimeRow
13 | {
14 |
15 | "Starts the timer using [[current]] time.
16 | Returns next fire time if successfull or null if completed."
17 | shared formal DateTime? start("current time" DateTime current);
18 |
19 | "Shifts time to the next one.
20 | Returns next fire time if successfull and null if completed."
21 | shared formal DateTime? shiftTime();
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/timer/TimeRowCronStyle.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.time {
2 |
3 | DateTime,
4 | dateTime
5 | }
6 |
7 | import ceylon.time.chronology {
8 |
9 | gregorian
10 | }
11 | import herd.schedule.chime.cron {
12 |
13 | CronExpression
14 | }
15 |
16 |
17 | "Ccron-like timer."
18 | since("0.1.0") by("Lis")
19 | class TimeRowCronStyle (
20 | "Cron expression rules the timer." CronExpression expression
21 | )
22 | satisfies TimeRow
23 | {
24 |
25 | // indexies of current time
26 | variable Iterator secondIndex = expression.seconds.iterator();
27 | variable Iterator minuteIndex = expression.minutes.iterator();
28 | variable Iterator hourIndex = expression.hours.iterator();
29 | variable Iterator dayIndex = expression.daysOfMonth.iterator();
30 | variable Iterator monthIndex = expression.months.iterator();
31 | variable Iterator yearIndex = expression.years.iterator();
32 |
33 | // current time
34 | variable Integer currentYear = 0;
35 | variable Integer currentMonth = 1;
36 | variable Integer currentDay = 1;
37 | variable Integer currentHour = 0;
38 | variable Integer currentMinute = 0;
39 | variable Integer currentSecond = 0;
40 |
41 | "current date and time"
42 | variable DateTime currentDate = dateTime( 0, 1, 1 );
43 |
44 | variable Boolean completed = false;
45 |
46 |
47 | "Completes the timer."
48 | void completeTimer() {
49 | completed = true;
50 | currentDate = dateTime(0, 1, 1);
51 | secondIndex = expression.seconds.iterator();
52 | minuteIndex = expression.minutes.iterator();
53 | hourIndex = expression.hours.iterator();
54 | dayIndex = expression.daysOfMonth.iterator();
55 | monthIndex = expression.months.iterator();
56 | yearIndex = expression.years.iterator();
57 | }
58 |
59 | "Starts seconds from beginning."
60 | void resetSeconds() {
61 | secondIndex = expression.seconds.iterator();
62 | if (is Integer val = secondIndex.next()) {
63 | currentSecond = val;
64 | }
65 | else {
66 | currentSecond = 0;
67 | }
68 | }
69 |
70 | "Starts minutes from beginning and reset seconds."
71 | void resetMinutes() {
72 | minuteIndex = expression.minutes.iterator();
73 | if (is Integer val = minuteIndex.next()) {
74 | currentMinute = val;
75 | }
76 | else {
77 | currentMinute = 0;
78 | }
79 | resetSeconds();
80 | }
81 |
82 | "Starts hours from beginning and reset minutes."
83 | void resetHours() {
84 | hourIndex = expression.hours.iterator();
85 | if (is Integer val = hourIndex.next()) {
86 | currentHour = val;
87 | }
88 | else {
89 | currentHour = 0;
90 | }
91 | resetMinutes();
92 | }
93 |
94 | "Start. days from beginning and reset hours."
95 | void resetDays() {
96 | dayIndex = expression.daysOfMonth.iterator();
97 | if (is Integer val = dayIndex.next()) {
98 | currentDay = val;
99 | }
100 | else {
101 | currentDay = 1;
102 | }
103 | resetHours();
104 | }
105 |
106 | "Starts days from beginning and reset and reset days."
107 | void resetMonth() {
108 | monthIndex = expression.months.iterator();
109 | if (is Integer val = monthIndex.next()) {
110 | currentMonth = val;
111 | }
112 | else {
113 | currentMonth = 1;
114 | }
115 | resetDays();
116 | }
117 |
118 | "Returns `true` if data is accepted and `false` otherwise."
119 | Boolean isDateAcepted() {
120 | value converted = gregorian.dateFrom( gregorian.fixedFrom( [currentYear, currentMonth, currentDay] ) );
121 | return converted[0] == currentYear && converted[1] == currentMonth && converted[2] == currentDay;
122 | }
123 |
124 | "Shifts year to next after the latest fire. Next year is one from specified in [[CronExpression.years]].
125 | If all years scooped out timing is completed.
126 | Returns `true` if completed."
127 | Boolean shiftYear() {
128 | // shift to next year
129 | if (!expression.years.empty) {
130 | if (is Integer item = yearIndex.next()) {
131 | currentYear = item;
132 | }
133 | else {
134 | return true;
135 | }
136 | }
137 | else {
138 | currentYear ++;
139 | }
140 | return false;
141 | }
142 |
143 | "Shifts month to next after the latest fire. Next month is one from specified in [[CronExpression.months]].
144 | If all months scooped out shifts year - [[shiftYear]].
145 | Returns `true` if completed and `false` otherwise."
146 | Boolean shiftMonth() {
147 | // shift to next month
148 | if (is Integer item = monthIndex.next()) {
149 | currentMonth = item;
150 | if (isDateAcepted()) {
151 | return false;
152 | }
153 | else {
154 | resetMonth();
155 | return shiftYear();
156 | }
157 | }
158 | else {
159 | resetMonth();
160 | return shiftYear();
161 | }
162 | }
163 |
164 | "Shifts day to next after the latest fire. Next day is one from specified in [[CronExpression.daysOfMonth]].
165 | If all days scooped out shifts month - [[shiftMonth]].
166 | Returns `true` if completed and `false` otherwise."
167 | Boolean shiftDay() {
168 | // shift to next day
169 | if (is Integer item = dayIndex.next()) {
170 | currentDay = item;
171 | if (isDateAcepted()) {
172 | return false;
173 | }
174 | else {
175 | resetDays();
176 | return shiftMonth();
177 | }
178 | }
179 | else {
180 | resetDays();
181 | return shiftMonth();
182 | }
183 | }
184 |
185 | "Shifts hours to next after the latest fire. Next hours are one from specified in [[CronExpression.hours]].
186 | If all hours scooped out shifts day - [[shiftDay]].
187 | Returns `true` if completed and `false` otherwise."
188 | Boolean shiftHour() {
189 | // shift to next hour
190 | if (is Integer item = hourIndex.next()) {
191 | currentHour = item;
192 | return false;
193 | }
194 | else {
195 | resetHours();
196 | return shiftDay();
197 | }
198 | }
199 |
200 | "Shifts minutes to next after the latest fire. Next minutes are one from specified in [[CronExpression.minutes]].
201 | If all minutes scooped out shifts hours - [[shiftHour]].
202 | Returns `true` if completed and `false` otherwise."
203 | Boolean shiftMinute() {
204 | // shift to next minute
205 | if (is Integer item = minuteIndex.next()) {
206 | currentMinute = item;
207 | return false;
208 | }
209 | else {
210 | resetMinutes();
211 | return shiftHour();
212 | }
213 | }
214 |
215 | "Shifts seconds to next after the latest fire. Next seconds are one from specified in [[CronExpression.seconds]].
216 | If all seconds scooped out shifts minutes - [[shiftMinute]].
217 | Returns `true` if completed and `false` otherwise."
218 | Boolean shiftSecond() {
219 | if (is Integer item = secondIndex.next()) {
220 | currentSecond = item;
221 | return false;
222 | }
223 | else {
224 | resetSeconds();
225 | return shiftMinute();
226 | }
227 |
228 | }
229 |
230 | "Considers weekdays in the [[currentDate]]. I.e. shifts days while [[currentDay]] is not within [[CronExpression.daysOfWeek]].
231 | Returns `false` if timer to be completed and `true` otherwise."
232 | Boolean considerWeekdays() {
233 | try {
234 | variable DateTime date = dateTime(currentYear, currentMonth, currentDay, currentHour, currentMinute, currentSecond);
235 | // shift to appropriate day of week
236 | variable Boolean reset = true;
237 | while (!expression.daysOfWeek.falls(date.date) && date != currentDate) {
238 | if ( reset ) {
239 | resetHours();
240 | reset = false;
241 | }
242 | if (shiftDay()) {
243 | completeTimer();
244 | return false;
245 | }
246 | date = dateTime(currentYear, currentMonth, currentDay, currentHour, currentMinute, currentSecond);
247 | }
248 | if (date != currentDate) {
249 | currentDate = date;
250 | return true;
251 | }
252 | else {
253 | completeTimer();
254 | return false;
255 | }
256 | }
257 | catch (Throwable err) {
258 | completeTimer();
259 | return false;
260 | }
261 | }
262 |
263 |
264 | "Starts timing from specified UTC time.
265 | Returns `true` if started and `false` if completed."
266 | Boolean startCron(DateTime current) {
267 | // find nearest time
268 |
269 | // year
270 | if (expression.years.empty) {
271 | currentYear = current.year;
272 | }
273 | else {
274 | currentYear = 0;
275 | yearIndex = expression.years.iterator();
276 | while (is Integer item = yearIndex.next()) {
277 | if (item >= current.year) {
278 | currentYear = item;
279 | break;
280 | }
281 | }
282 | if (currentYear == 0) {
283 | completeTimer();
284 | return false;
285 | }
286 | else if (currentYear > current.year) {
287 | resetMonth();
288 | return considerWeekdays();
289 | }
290 | }
291 |
292 | // month
293 | currentMonth = 0;
294 | monthIndex = expression.months.iterator();
295 | while (is Integer item = monthIndex.next()) {
296 | if (item >= current.month.integer) {
297 | currentMonth = item;
298 | break;
299 | }
300 | }
301 | if (currentMonth == 0) {
302 | resetMonth();
303 | if (shiftYear()) {
304 | completeTimer();
305 | return false;
306 | }
307 | else {
308 | return considerWeekdays();
309 | }
310 | }
311 | if (currentMonth > current.month.integer) {
312 | resetDays();
313 | return considerWeekdays();
314 | }
315 |
316 | // day
317 | currentDay = 0;
318 | dayIndex = expression.daysOfMonth.iterator();
319 | while (is Integer item = dayIndex.next()) {
320 | if (item >= current.day) {
321 | currentDay = item;
322 | break;
323 | }
324 | }
325 | if (currentDay == 0) {
326 | resetDays();
327 | if (shiftMonth()) {
328 | completeTimer();
329 | return false;
330 | }
331 | else {
332 | return considerWeekdays();
333 | }
334 | }
335 | if (currentDay > current.day) {
336 | resetHours();
337 | return considerWeekdays();
338 | }
339 |
340 | // hour
341 | currentHour = -1;
342 | hourIndex = expression.hours.iterator();
343 | while (is Integer item = hourIndex.next()) {
344 | if (item >= current.hours) {
345 | currentHour = item;
346 | break;
347 | }
348 | }
349 | if (currentHour == -1) {
350 | resetHours();
351 | if (shiftDay()) {
352 | completeTimer();
353 | return false;
354 | }
355 | else {
356 | return considerWeekdays();
357 | }
358 | }
359 | if (currentHour > current.hours) {
360 | resetMinutes();
361 | return considerWeekdays();
362 | }
363 |
364 | // minutes
365 | currentMinute = -1;
366 | minuteIndex = expression.minutes.iterator();
367 | while (is Integer item = minuteIndex.next()) {
368 | if (item >= current.minutes) {
369 | currentMinute = item;
370 | break;
371 | }
372 | }
373 | if (currentMinute == -1) {
374 | resetMinutes();
375 | if ( shiftHour() ) {
376 | completeTimer();
377 | return false;
378 | }
379 | else {
380 | return considerWeekdays();
381 | }
382 | }
383 | if (currentMinute > current.minutes) {
384 | resetSeconds();
385 | return considerWeekdays();
386 | }
387 |
388 | // seconds
389 | currentSecond = -1;
390 | secondIndex = expression.seconds.iterator();
391 | while (is Integer item = secondIndex.next()) {
392 | if (item >= current.seconds) {
393 | currentSecond = item;
394 | break;
395 | }
396 | }
397 | if (currentSecond == -1) {
398 | resetSeconds();
399 | if (shiftMinute()) {
400 | completeTimer();
401 | return false;
402 | }
403 | }
404 |
405 | // shift to appropriate day of week
406 | return considerWeekdays();
407 |
408 | }
409 |
410 | "Calculates next local time and stores it in [[currentDate]].
411 | Returns `true` if successfully shifted and `false` if to be completed."
412 | Boolean shiftCronTime() {
413 | if (shiftSecond()) {
414 | completeTimer();
415 | return false;
416 | }
417 | else {
418 | return considerWeekdays();
419 | }
420 | }
421 |
422 |
423 | /* Timer interface */
424 |
425 | shared actual DateTime? start(DateTime current) {
426 | if (startCron(current)) {
427 | return currentDate;
428 | }
429 | else {
430 | return null;
431 | }
432 | }
433 |
434 | shared actual DateTime? shiftTime() {
435 | if (shiftCronTime()) {
436 | return currentDate;
437 | }
438 | else {
439 | return null;
440 | }
441 | }
442 |
443 | shared actual String string => "cron time row";
444 |
445 | }
446 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/timer/TimeRowFactory.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.json {
2 |
3 | JsonObject
4 | }
5 | import herd.schedule.chime.service {
6 | ChimeServices,
7 | Extension
8 | }
9 |
10 |
11 | "Creates [[TimeRow]]."
12 | since("0.1.0") by("Lis")
13 | shared interface TimeRowFactory satisfies Extension
14 | {
15 |
16 | "Creates new time row. Returns created [[TimeRow]] or error code -> message pair if some error occured."
17 | shared actual formal TimeRow|String> create (
18 | "Provides Chime services." ChimeServices services,
19 | "Timer description." JsonObject description
20 | );
21 |
22 | "Timer type provided with timer create request, see [[module herd.schedule.chime]]."
23 | shared actual formal String type;
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/timer/TimeRowInterval.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.time {
2 |
3 | DateTime,
4 | dateTime
5 | }
6 |
7 |
8 | "Incremental timer - starts from specific time date
9 | and increments on certain miliseconds each time when fired - [[intervalMilliseconds]].
10 | [[intervalMilliseconds]] to be >= 0.
11 | "
12 | since("0.1.0") by("Lis")
13 | class TimeRowInterval (
14 | "Timing delay in miliseconds, to be >= 0." shared Integer intervalMilliseconds
15 | )
16 | satisfies TimeRow
17 | {
18 | "Current date and time."
19 | variable DateTime currentDate = dateTime(0, 1, 1);
20 |
21 |
22 | shared actual DateTime? start(DateTime current) => currentDate = current.plusMilliseconds(intervalMilliseconds);
23 |
24 | shared actual DateTime? shiftTime() => currentDate = currentDate.plusMilliseconds(intervalMilliseconds);
25 |
26 | shared actual String string => "interval time row with delay of ``intervalMilliseconds`` ms";
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/timer/TimeRowUnion.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.time {
2 | DateTime
3 | }
4 | import ceylon.collection {
5 | ArrayList
6 | }
7 |
8 |
9 | "Represents a union of the time rows."
10 | since("0.2.1") by("Lis")
11 | shared class TimeRowUnion satisfies TimeRow {
12 |
13 | static class InternalRow(TimeRow row) satisfies TimeRow {
14 | variable DateTime? currentDate_ = null;
15 | shared DateTime? currentDate => currentDate_;
16 | shared actual DateTime? shiftTime() {
17 | return (currentDate_ = row.shiftTime());
18 | }
19 | shared actual DateTime? start(DateTime current) {
20 | return (currentDate_ = row.start(current));
21 | }
22 | shared actual String string => row.string;
23 | }
24 |
25 | static object emptyInternalRow extends InternalRow(emptyTimeRow) {}
26 |
27 |
28 | "Currently selected row, i.e. with min date. Selected at [[shiftTime]] or [[start]]."
29 | variable InternalRow currentRow = emptyInternalRow;
30 | "List of currently active time rows."
31 | ArrayList timeRows;
32 |
33 |
34 | "Instantiates new time row union from a list of time rows."
35 | shared new ("Time rows for union." {TimeRow+} rows) {
36 | timeRows = ArrayList{for (item in rows) InternalRow(item)};
37 | }
38 |
39 |
40 | "Removes completed time rows from [[timeRows]] list."
41 | void removeCompleted() {
42 | value toRemove = [for (item in timeRows) if (!item.currentDate exists) item];
43 | timeRows.removeAll(toRemove);
44 | if (timeRows.empty) {
45 | currentRow = emptyInternalRow;
46 | }
47 | }
48 |
49 |
50 | shared actual DateTime? shiftTime() {
51 | currentRow.shiftTime();
52 | currentRow = emptyInternalRow;
53 | for (item in timeRows) {
54 | if (exists n = currentRow.currentDate) {
55 | if (exists itemDate = item.currentDate, itemDate < n) {
56 | currentRow = item;
57 | }
58 | }
59 | else {
60 | currentRow = item;
61 | }
62 | }
63 | value ret = currentRow.currentDate;
64 | removeCompleted();
65 | return ret;
66 | }
67 |
68 | shared actual DateTime? start(DateTime current) {
69 | currentRow = emptyInternalRow;
70 | for (item in timeRows) {
71 | if (exists itemDate = item.start(current)) {
72 | if (exists l = currentRow.currentDate) {
73 | if (itemDate < l) {
74 | currentRow = item;
75 | }
76 | }
77 | else {
78 | currentRow = item;
79 | }
80 | }
81 | }
82 | value ret = currentRow.currentDate;
83 | removeCompleted();
84 | return ret;
85 | }
86 |
87 | shared actual String string => "union time row: ``timeRows``";
88 |
89 | }
90 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/timer/UnionFactory.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.collection {
2 | ArrayList
3 | }
4 | import ceylon.json {
5 | JsonObject,
6 | JsonArray
7 | }
8 | import herd.schedule.chime {
9 | Chime
10 | }
11 | import herd.schedule.chime.service {
12 | ChimeServices,
13 | Extension
14 | }
15 |
16 |
17 | "Factory to create union timers."
18 | service(`interface Extension`)
19 | since("0.3.0") by("Lis")
20 | shared class UnionFactory() satisfies TimeRowFactory
21 | {
22 |
23 | shared actual String type => Chime.type.union;
24 |
25 | shared actual TimeRow|String> create(ChimeServices services, JsonObject description) {
26 | if (is JsonArray timers = description[Chime.key.timers]) {
27 | ArrayList timeRows = ArrayList();
28 | for (timer in timers) {
29 | if (is JsonObject timer) {
30 | value ret = services.createTimeRow(timer);
31 | if (is TimeRow ret) {
32 | timeRows.add(ret);
33 | }
34 | else {
35 | return ret;
36 | }
37 | }
38 | else {
39 | return Chime.errors.codeNotJSONTimerDescription->Chime.errors.notJSONTimerDescription;
40 | }
41 | }
42 | if (nonempty unionRows = timeRows.sequence()) {
43 | return TimeRowUnion(unionRows);
44 | }
45 | else {
46 | return Chime.errors.codeTimersListHasToBeSpecified->Chime.errors.timersListHasToBeSpecified;
47 | }
48 | }
49 | else {
50 | return Chime.errors.codeTimersListHasToBeSpecified->Chime.errors.timersListHasToBeSpecified;
51 | }
52 | }
53 |
54 | shared actual String string => "union time row factory";
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/timer/emptyTimeRow.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.time {
2 | DateTime
3 | }
4 |
5 |
6 | "`TimeRow` which return `null`."
7 | since("0.3.0") by("Lis")
8 | object emptyTimeRow satisfies TimeRow {
9 | shared actual DateTime? shiftTime() => null;
10 | shared actual DateTime? start(DateTime current) => null;
11 | }
12 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/timer/package.ceylon:
--------------------------------------------------------------------------------
1 | "Provides timer services.
2 |
3 | Built-in and custom timers are provided with [[TimeRow]] interface.
4 | The interface is similar to time enumerator, but allows to restart enumeration from some time.
5 | [[TimeRow]] is created using service provider represented with [[TimeRowFactory]] interface.
6 |
7 | Built-in time rows and factories:
8 | * cron-style time row created by [[CronFactory]]
9 | * interval time row created by [[IntervalFactory]]
10 | * union time row created by [[UnionFactory]]
11 |
12 | To build your own [[TimeRow]] (i.e. timer) follow the steps:
13 | 1. Implement [[TimeRowFactory]].
14 | 2. Mark the class from 1. with `service(`\` `interface Extension`\` `)`.
15 | 3. Deploy _Chime_ with configuration provided modules to serach the services:
16 | JsonObject {
17 | \"services\" -> [\"module name/module version\"]
18 | }
19 |
20 | "
21 | since("0.3.0") by("Lis")
22 | shared package herd.schedule.chime.service.timer;
23 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/timezone/JVMTimeZone.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.time {
2 | Instant,
3 | DateTime
4 | }
5 | import java.util {
6 | JavaTimeZone=TimeZone
7 | }
8 |
9 |
10 | "Converts date/time using time zones available at JVM according to specified remote time zone."
11 | since("0.1.0") by("Lis")
12 | class JVMTimeZone satisfies TimeZone {
13 |
14 | static JavaTimeZone localtz = JavaTimeZone.default;
15 |
16 | JavaTimeZone remoteTimeZone;
17 |
18 | shared new ("Time zone to link this one to." JavaTimeZone remoteTimeZone) {
19 | this.remoteTimeZone = remoteTimeZone;
20 | }
21 |
22 | shared actual DateTime toLocal(DateTime remote) {
23 | Integer remoteTime = remote.instant().millisecondsOfEpoch;
24 | Integer utcTime = remoteTime - remoteTimeZone.getOffset(remoteTime);
25 | Integer localUTCOffset = localtz.getOffset(utcTime + localtz.getOffset(utcTime));
26 | return Instant(utcTime + localUTCOffset).dateTime();
27 | }
28 |
29 | shared actual DateTime toRemote(DateTime local) {
30 | Integer localTime = local.instant().millisecondsOfEpoch;
31 | Integer utcTime = localTime - localtz.getOffset(localTime);
32 | Integer remoteUTCOffset = remoteTimeZone.getOffset(utcTime + remoteTimeZone.getOffset(utcTime));
33 | return Instant(utcTime + remoteUTCOffset).dateTime();
34 | }
35 |
36 | shared actual String timeZoneID => remoteTimeZone.id;
37 |
38 | shared actual String string => timeZoneID;
39 | }
40 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/timezone/JVMTimeZoneFactory.ceylon:
--------------------------------------------------------------------------------
1 | import herd.schedule.chime {
2 | Chime
3 | }
4 | import java.util {
5 | JavaTimeZone=TimeZone
6 | }
7 | import herd.schedule.chime.service {
8 | ChimeServices,
9 | Extension
10 | }
11 | import ceylon.time {
12 | DateTime
13 | }
14 | import ceylon.json {
15 | JsonObject
16 | }
17 |
18 |
19 | "Creates [[TimeZone]] using JVM time zones."
20 | service(`interface Extension`)
21 | since("0.3.0") by( "Lis")
22 | shared class JVMTimeZoneFactory satisfies TimeZoneFactory
23 | {
24 |
25 | "Defines time zone which does no convertion."
26 | shared static object localTimeZone satisfies TimeZone {
27 | shared actual DateTime toLocal(DateTime remote) => remote;
28 | shared actual DateTime toRemote(DateTime local) => local;
29 | shared actual String timeZoneID => JavaTimeZone.default.id;
30 | }
31 |
32 | "New `JVMTimeZoneFactory`."
33 | shared new () {}
34 |
35 | shared actual TimeZone|String> create(ChimeServices services, JsonObject options) {
36 | if (is String timeZone = options[Chime.key.timeZone]) {
37 | JavaTimeZone tz = JavaTimeZone.getTimeZone(timeZone);
38 | if (tz.id == timeZone) {
39 | return JVMTimeZone(tz);
40 | }
41 | else {
42 | return Chime.errors.codeUnsupportedTimezone->Chime.errors.unsupportedTimezone;
43 | }
44 | }
45 | else {
46 | return localTimeZone;
47 | }
48 |
49 | }
50 |
51 | shared actual String type => Chime.timeZoneProvider.jvm;
52 |
53 | shared actual String string => "JVM time zone factory";
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/timezone/TimeZone.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.time {
2 | DateTime
3 | }
4 |
5 |
6 | "Convertes date-time according to rule (timezone).
7 | Generally, a time zone is instantiated by [[TimeZoneFactory]] given as service provider,
8 | see [[package herd.schedule.chime.service]]."
9 | see(`interface TimeZoneFactory`)
10 | since("0.3.0") by("Lis")
11 | shared interface TimeZone {
12 |
13 | "Converts remote (this time zone) date-time to local (for the current machine) one."
14 | shared formal DateTime toLocal("Date-time to convert from." DateTime remote);
15 |
16 | "Converts local (for the current machine) date-time to remote (to this time zone) one."
17 | shared formal DateTime toRemote("Date-time to convert from." DateTime local);
18 |
19 | "Returns time zone id."
20 | shared formal String timeZoneID;
21 | }
22 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/timezone/TimeZoneFactory.ceylon:
--------------------------------------------------------------------------------
1 | import herd.schedule.chime.service {
2 | Extension,
3 | ChimeServices
4 | }
5 | import ceylon.json {
6 | JsonObject
7 | }
8 |
9 |
10 | "Time zone provider - creates [[TimeZone]]."
11 | since("0.3.0") by("Lis")
12 | shared interface TimeZoneFactory satisfies Extension
13 | {
14 | "Creates new time zone with the given time zone name.
15 | Returns created [[TimeZone]] or error code -> message pair if some error occured."
16 | shared actual formal TimeZone|String> create (
17 | "Provides Chime services." ChimeServices services,
18 | "Options with \"time zone\" key given." JsonObject options
19 | );
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/source/herd/schedule/chime/service/timezone/package.ceylon:
--------------------------------------------------------------------------------
1 |
2 | "Provides time zone services.
3 |
4 | [[TimeZone]] interface provides converting time from / to the given time zone.
5 |
6 | built-in time zones:
7 | * time zones available at jvm, created by [[JVMTimeZoneFactory]].
8 |
9 | To build your own [[TimeZone]] follow the steps:
10 | 1. Implement [[TimeZoneFactory]].
11 | 2. Mark the class from 1. with `service(`\` `interface TimeZoneFactory`\` `)`.
12 | 3. Deploy _Chime_ with configuration provided modules to serach the services:
13 | JsonObject {
14 | \"services\" -> [\"module name/module version\"]
15 | }
16 |
17 | In timer or scheduler create request time zone type can be specified under \"time zone provider\" key.
18 |
19 | > At scheduler level default time zone may be specified, which is applied to timer if no time zone
20 | is given at timer create request.
21 | "
22 | since("0.3.0") by("Lis")
23 | shared package herd.schedule.chime.service.timezone;
24 |
--------------------------------------------------------------------------------
/test/herd/test/schedule/chime/CronBuilder.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.test {
2 | test
3 | }
4 | import herd.asynctest {
5 | AsyncTestContext
6 | }
7 | import herd.schedule.chime {
8 | CronBuilder,
9 | Chime
10 | }
11 | import ceylon.json {
12 |
13 | JsonObject
14 | }
15 | import herd.asynctest.match {
16 | ItemByKey,
17 | ExceptionHasType
18 | }
19 |
20 | class CronBuilderTest() {
21 |
22 | shared test void cronBuilderWith( AsyncTestContext context ) {
23 | JsonObject cron = CronBuilder().withSeconds(1,2).withMinutes(1,2).withHours(1,2)
24 | .withDays(1,2).withMonths(1,2).withYears(2017).withDaysOfWeek(1,2).build();
25 | context.assertThat (
26 | cron,
27 | ItemByKey( "type", Chime.type.cron )
28 | );
29 | context.assertThat (
30 | cron,
31 | ItemByKey( "seconds", "1,2" )
32 | );
33 | context.assertThat (
34 | cron,
35 | ItemByKey( "minutes", "1,2" )
36 | );
37 | context.assertThat (
38 | cron,
39 | ItemByKey( "hours", "1,2" )
40 | );
41 | context.assertThat (
42 | cron,
43 | ItemByKey( "days of month", "1,2" )
44 | );
45 | context.assertThat (
46 | cron,
47 | ItemByKey( "months", "1,2" )
48 | );
49 | context.assertThat (
50 | cron,
51 | ItemByKey( "years", "2017" )
52 | );
53 | context.assertThat (
54 | cron,
55 | ItemByKey( "days of week", "1,2" )
56 | );
57 | context.complete();
58 | }
59 |
60 | shared test void cronBuilderWithAll( AsyncTestContext context ) {
61 | JsonObject cron = CronBuilder().withAllSeconds().withAllMinutes().withAllHours()
62 | .withAllDays().withAllMonths().withAllYears().withAllDaysOfWeek().build();
63 | context.assertThat (
64 | cron,
65 | ItemByKey( "type", Chime.type.cron )
66 | );
67 | context.assertThat (
68 | cron,
69 | ItemByKey( "seconds", "*" )
70 | );
71 | context.assertThat (
72 | cron,
73 | ItemByKey( "minutes", "*" )
74 | );
75 | context.assertThat (
76 | cron,
77 | ItemByKey( "hours", "*" )
78 | );
79 | context.assertThat (
80 | cron,
81 | ItemByKey( "days of month", "*" )
82 | );
83 | context.assertThat (
84 | cron,
85 | ItemByKey( "months", "*" )
86 | );
87 | context.assertThat (
88 | cron,
89 | ItemByKey( "years", "*" )
90 | );
91 | context.assertThat (
92 | cron,
93 | ItemByKey( "days of week", "*" )
94 | );
95 | context.complete();
96 | }
97 |
98 | shared test void cronBuilderWithRange( AsyncTestContext context ) {
99 | JsonObject cron = CronBuilder().withSecondsRange(1,5,2).withMinutesRange(1,5,2).withHoursRange(1,5,2)
100 | .withDaysRange(1,5,2).withMonthsRange(1,5,2).withYearsRange(2017,2021,2).withDaysOfWeekRange(1,5,2).build();
101 | context.assertThat (
102 | cron,
103 | ItemByKey( "type", Chime.type.cron )
104 | );
105 | context.assertThat (
106 | cron,
107 | ItemByKey( "seconds", "1-5/2" )
108 | );
109 | context.assertThat (
110 | cron,
111 | ItemByKey( "minutes", "1-5/2" )
112 | );
113 | context.assertThat (
114 | cron,
115 | ItemByKey( "hours", "1-5/2" )
116 | );
117 | context.assertThat (
118 | cron,
119 | ItemByKey( "days of month", "1-5/2" )
120 | );
121 | context.assertThat (
122 | cron,
123 | ItemByKey( "months", "1-5/2" )
124 | );
125 | context.assertThat (
126 | cron,
127 | ItemByKey( "years", "2017-2021/2" )
128 | );
129 | context.assertThat (
130 | cron,
131 | ItemByKey( "days of week", "1-5/2" )
132 | );
133 | context.complete();
134 | }
135 |
136 | shared test void cronBuilderFromBuilder( AsyncTestContext context ) {
137 | CronBuilder cronBuilder = CronBuilder().withSeconds(1,2).withMinutes(1,2).withHours(1,2);
138 | JsonObject cron = CronBuilder.fromBuilder(cronBuilder).withDays(1,2).withMonths(1,2).build();
139 | context.assertThat (
140 | cron,
141 | ItemByKey( "type", Chime.type.cron )
142 | );
143 | context.assertThat (
144 | cron,
145 | ItemByKey( "seconds", "1,2" )
146 | );
147 | context.assertThat (
148 | cron,
149 | ItemByKey( "minutes", "1,2" )
150 | );
151 | context.assertThat (
152 | cron,
153 | ItemByKey( "hours", "1,2" )
154 | );
155 | context.assertThat (
156 | cron,
157 | ItemByKey( "days of month", "1,2" )
158 | );
159 | context.assertThat (
160 | cron,
161 | ItemByKey( "months", "1,2" )
162 | );
163 | context.complete();
164 | }
165 | shared test void cronBuilderIncomplete( AsyncTestContext context ) {
166 | CronBuilder cron = CronBuilder().withSecondsRange(1,5,2);
167 | context.assertThatException (
168 | cron.build,
169 | ExceptionHasType()
170 | );
171 | context.complete();
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/test/herd/test/schedule/chime/SchedulerTimer.ceylon:
--------------------------------------------------------------------------------
1 | import ceylon.test {
2 | beforeTestRun,
3 | afterTestRun,
4 | test
5 | }
6 | import herd.asynctest {
7 | AsyncPrePostContext,
8 | AsyncTestContext
9 | }
10 | import io.vertx.ceylon.core {
11 | Vertx,
12 | vertx
13 | }
14 | import herd.schedule.chime {
15 | Chime,
16 | Scheduler,
17 | createScheduler,
18 | schedulerInfo,
19 | delete,
20 | Timer,
21 | TimerEvent,
22 | TimerInfo,
23 | SchedulerInfo,
24 | TimerFire,
25 | TimerCompleted
26 | }
27 | import io.vertx.ceylon.core.eventbus {
28 | EventBus,
29 | Message
30 | }
31 | import herd.asynctest.match {
32 | EqualTo,
33 | ExceptionHasMessage,
34 | PassType,
35 | SizeOf
36 | }
37 | import ceylon.json {
38 | JsonObject
39 | }
40 |
41 |
42 | "Testing `Scheduler` and `Timer` interfaces and scheduler top level functions."
43 | since( "0.2.0" ) by( "Lis" )
44 | shared class SchedulerTimer()
45 | {
46 | String chime = "chime";
47 | Vertx v = vertx.vertx();
48 | EventBus eventBus = v.eventBus();
49 |
50 |
51 | shared afterTestRun void dispose() {
52 | v.close();
53 | }
54 |
55 | shared beforeTestRun void initialize(AsyncPrePostContext initContext) {
56 | Chime c = Chime();
57 | c.deploy (
58 | v, null,
59 | (Throwable|String res) {
60 | if (is String res) {
61 | initContext.proceed();
62 | }
63 | else {
64 | initContext.abort(res, "Chime starting");
65 | }
66 | }
67 | );
68 |
69 | }
70 |
71 | test shared void createDeleteScheduler(AsyncTestContext context) {
72 | String schedulerName = "createDeleteScheduler";
73 | createScheduler (
74 | (Throwable|Scheduler msg) {
75 | if (is Scheduler msg) {
76 | context.assertThat(msg.name, EqualTo( schedulerName));
77 | msg.delete();
78 | eventBus.send (
79 | chime,
80 | JsonObject {
81 | Chime.key.operation -> Chime.operation.info,
82 | Chime.key.name -> schedulerName
83 | },
84 | (Throwable|Message msg) {
85 | context.assertThat(msg, PassType(ExceptionHasMessage(Chime.errors.schedulerNotExists)));
86 | context.complete();
87 | }
88 | );
89 | }
90 | else {
91 | context.fail(msg);
92 | context.complete();
93 | }
94 | },
95 | chime, eventBus, schedulerName
96 | );
97 | }
98 |
99 |
100 | test shared void createDeleteTimer(AsyncTestContext context) {
101 | String schedulerName = "createDeleteTimer";
102 | createScheduler (
103 | (Throwable|Scheduler msg) {
104 | if (is Scheduler msg) {
105 | msg.createIntervalTimer (
106 | (Throwable|Timer timer) {
107 | if (is Timer timer) {
108 | timer.handler (
109 | (TimerEvent event) {
110 | timer.delete();
111 | timer.info (
112 | (Throwable|TimerInfo info) {
113 | context.assertThat (
114 | info, PassType(ExceptionHasMessage(Chime.errors.timerNotExists))
115 | );
116 | msg.delete();
117 | context.complete();
118 | }
119 | );
120 | }
121 | );
122 | }
123 | else {
124 | msg.delete();
125 | context.fail(timer);
126 | context.complete();
127 | }
128 | },
129 | 1
130 | );
131 | }
132 | else {
133 | context.fail(msg);
134 | context.complete();
135 | }
136 | },
137 | chime, eventBus, schedulerName
138 | );
139 | }
140 |
141 |
142 | void createTimer(String schedulerName, String timerName, Integer delay) {
143 | eventBus.send (
144 | chime,
145 | JsonObject {
146 | Chime.key.operation -> Chime.operation.create,
147 | Chime.key.name -> schedulerName + Chime.configuration.nameSeparator + timerName,
148 | Chime.key.description -> JsonObject {
149 | Chime.key.type -> Chime.type.interval,
150 | Chime.key.delay -> delay
151 | }
152 | }
153 | );
154 | }
155 |
156 |
157 | test shared void deleteTimers(AsyncTestContext context) {
158 | String scheduler1 = "scheduler1";
159 | String scheduler2 = "scheduler2";
160 | String timer1 = "timer1";
161 | String timer2 = "timer2";
162 | String timer1Full = scheduler1 + ":" + timer1;
163 | String timer2Full = scheduler2 + ":" + timer2;
164 |
165 | // deletes all schedulers if created before this test
166 | delete( chime, eventBus );
167 |
168 | createTimer(scheduler1, timer1, 5);
169 | createTimer(scheduler1, timer2, 7);
170 | createTimer(scheduler2, timer1, 4);
171 | createTimer(scheduler2, timer2, 9);
172 |
173 | delete (
174 | chime, eventBus, {timer1Full, timer2Full},
175 | (Throwable|{String*} msg) {
176 | if (is Throwable msg) {
177 | delete(chime, eventBus);
178 | context.fail(msg);
179 | context.complete();
180 | }
181 | else {
182 | context.assertThat(msg, SizeOf(2));
183 | if (exists timer1Name = msg.first, exists timer2Name = msg.last) {
184 | context.assertThat(timer1Name, EqualTo( timer1Full));
185 | context.assertThat(timer2Name, EqualTo( timer2Full));
186 | }
187 | else {
188 | context.fail(AssertionError("Returned list of deleted items is empty."));
189 | }
190 | delete(chime, eventBus);
191 | context.complete();
192 | }
193 | }
194 | );
195 | }
196 |
197 |
198 | test shared void deleteSchedulers(AsyncTestContext context) {
199 | String scheduler1 = "scheduler1";
200 | String scheduler2 = "scheduler2";
201 | String timer1 = "timer1";
202 | String timer2 = "timer2";
203 |
204 | // deletes all schedulers if created before this test
205 | delete( chime, eventBus );
206 |
207 | createTimer(scheduler1, timer1, 5);
208 | createTimer(scheduler1, timer2, 7);
209 | createTimer(scheduler2, timer1, 4);
210 | createTimer(scheduler2, timer2, 9);
211 |
212 | delete (
213 | chime, eventBus, {scheduler1, scheduler2},
214 | (Throwable|{String*} msg) {
215 | if (is Throwable msg) {
216 | delete(chime, eventBus);
217 | context.fail(msg);
218 | context.complete();
219 | }
220 | else {
221 | context.assertThat(msg, SizeOf(2));
222 | if (exists scheduler1Name = msg.first, exists scheduler2Name = msg.last) {
223 | context.assertThat(scheduler1Name, EqualTo( scheduler1));
224 | context.assertThat(scheduler2Name, EqualTo( scheduler2));
225 | }
226 | else {
227 | context.fail(AssertionError("Returned list of deleted items is empty."));
228 | }
229 | delete(chime, eventBus);
230 | context.complete();
231 | }
232 | }
233 | );
234 | }
235 |
236 |
237 | test shared void getSchedulerInfo(AsyncTestContext context) {
238 | String scheduler1 = "info1";
239 | String scheduler2 = "info2";
240 | String timer1 = "timer1";
241 | String timer2 = "timer2";
242 |
243 | // deletes all schedulers if created before this test
244 | delete(chime, eventBus);
245 |
246 | createTimer(scheduler1, timer1, 5);
247 | createTimer(scheduler1, timer2, 7);
248 | createTimer(scheduler2, timer1, 4);
249 | createTimer(scheduler2, timer2, 9);
250 |
251 | schedulerInfo (
252 | (Throwable|SchedulerInfo[] msg) {
253 | if (is Throwable msg) {
254 | delete(chime, eventBus);
255 | context.fail(msg);
256 | context.complete();
257 | }
258 | else {
259 | context.assertThat(msg, SizeOf(2));
260 | if (exists sch1 = msg.first, exists sch2 = msg.last) {
261 | context.assertThat(sch1.timers, SizeOf(2));
262 | context.assertThat(sch2.timers, SizeOf(2));
263 | context.assertThat(sch1.name, EqualTo( scheduler1));
264 | context.assertThat(sch2.name, EqualTo( scheduler2));
265 | }
266 | else {
267 | context.fail(AssertionError("Returned list of infos is empty."));
268 | }
269 | delete(chime, eventBus);
270 | context.complete();
271 | }
272 | },
273 | chime, eventBus
274 | );
275 | }
276 |
277 | test shared void timerMessage(AsyncTestContext context) {
278 | String schedulerName = "TimerMessage";
279 | String timerMessage = "message";
280 | createScheduler (
281 | (Throwable|Scheduler msg) {
282 | if (is Scheduler msg) {
283 | msg.createIntervalTimer {
284 | handler = (Throwable|Timer timer) {
285 | if (is Timer timer) {
286 | timer.handler (
287 | (TimerEvent event) {
288 | switch (event)
289 | case (is TimerFire) {
290 | context.assertThat (
291 | event.message, PassType(EqualTo(timerMessage))
292 | );
293 | }
294 | case (is TimerCompleted) {
295 | msg.delete();
296 | context.complete();
297 | }
298 |
299 | timer.delete();
300 | timer.info (
301 | (Throwable|TimerInfo info) {
302 | context.assertThat (
303 | info, PassType(ExceptionHasMessage(Chime.errors.timerNotExists))
304 | );
305 | msg.delete();
306 | context.complete();
307 | }
308 | );
309 | }
310 | );
311 | }
312 | else {
313 | msg.delete();
314 | context.fail(timer);
315 | context.complete();
316 | }
317 | };
318 | delay = 1;
319 | maxCount = 1;
320 | message = timerMessage;
321 | };
322 | }
323 | else {
324 | context.fail(msg);
325 | context.complete();
326 | }
327 | },
328 | chime, eventBus, schedulerName
329 | );
330 | }
331 |
332 | }
333 |
--------------------------------------------------------------------------------
/test/herd/test/schedule/chime/SimpleTimers.ceylon:
--------------------------------------------------------------------------------
1 | import herd.asynctest {
2 |
3 | AsyncTestContext,
4 | AsyncPrePostContext
5 | }
6 | import io.vertx.ceylon.core {
7 |
8 | Vertx,
9 | vertx
10 | }
11 | import io.vertx.ceylon.core.eventbus {
12 |
13 | EventBus,
14 | Message
15 | }
16 | import ceylon.test {
17 |
18 | test,
19 | afterTestRun,
20 | beforeTestRun
21 | }
22 | import ceylon.json {
23 |
24 | JsonObject,
25 | JsonArray
26 | }
27 | import herd.asynctest.match {
28 |
29 | EqualTo,
30 | PassType,
31 | NotEmpty
32 | }
33 | import herd.schedule.chime {
34 |
35 | Chime
36 | }
37 |
38 |
39 | since( "0.2.0" ) by( "Lis" )
40 | shared class SimpleTimers()
41 | {
42 |
43 | String chime = "chime";
44 |
45 | String scheduler = "scheduler";
46 |
47 | String interval = "scheduler:interval";
48 | String cron = "scheduler:cron";
49 | String union = "scheduler:union";
50 |
51 |
52 | Vertx v = vertx.vertx();
53 | EventBus eventBus = v.eventBus();
54 |
55 |
56 | shared afterTestRun void dispose() {
57 | v.close();
58 | }
59 |
60 | shared beforeTestRun void initialize(AsyncPrePostContext initContext) {
61 | Chime c = Chime();
62 | c.deploy (
63 | v, null,
64 | (String|Throwable res) {
65 | if (is String res) {
66 | setupScheduler(initContext);
67 | }
68 | else {
69 | initContext.abort(res, "Chime starting");
70 | }
71 | }
72 | );
73 |
74 | }
75 |
76 | void setupScheduler(AsyncPrePostContext initContext) {
77 | eventBus.send (
78 | chime,
79 | JsonObject {
80 | Chime.key.operation -> Chime.operation.create,
81 | Chime.key.name -> scheduler,
82 | Chime.key.state -> Chime.state.running
83 | },
84 | (Throwable | Message msg) {
85 | if (is Message msg) {
86 | initContext.proceed();
87 | }
88 | else {
89 | initContext.abort(msg, "Scheduler creation");
90 | }
91 | }
92 | );
93 |
94 | }
95 |
96 |
97 | Anything(Throwable | Message) timerValidation (
98 | String timerName, Integer delay, Integer max, AsyncTestContext context
99 | ) {
100 | variable Integer fireCount = 0;
101 | variable Integer? previousTime = null;
102 | variable Integer totalDelay = 0;
103 | return (Throwable | Message msg) {
104 | if (is Message msg) {
105 | if (exists body = msg.body()) {
106 | if (is String event = body[Chime.key.event]) {
107 | if (event == Chime.event.complete) {
108 | context.assertThat(fireCount, EqualTo(max), "Total number of fires for ``timerName``");
109 | context.assertThat(totalDelay, EqualTo(delay * (max - 1)), "Total delay seconds for ``timerName``");
110 | context.complete();
111 | }
112 | else if (event == Chime.event.fire) {
113 | fireCount ++;
114 | if (is Integer seconds = body[Chime.date.seconds]) {
115 | if (exists prev = previousTime) {
116 | if (seconds > prev) {
117 | totalDelay += seconds - prev;
118 | }
119 | else {
120 | totalDelay += seconds + 60 - prev;
121 | }
122 | }
123 | previousTime = seconds;
124 | }
125 | else {
126 | context.fail (
127 | Exception("Chime timer fires without timer seconds ('seconds' field)"),
128 | "Chime timer ``timerName`` fire"
129 | );
130 | context.complete();
131 | }
132 | }
133 | else {
134 | context.fail (
135 | Exception("Chime timer event has to be one of FIRE or COMPLETE"),
136 | "Chime timer ``timerName`` fire"
137 | );
138 | context.complete();
139 | }
140 | }
141 | else {
142 | context.fail (
143 | Exception("Chime timer event without timer event specification ('event' field)"),
144 | "Chime timer ``timerName`` event"
145 | );
146 | context.complete();
147 | }
148 | }
149 | else {
150 | context.fail (
151 | Exception("Chime timer fires with null message"),
152 | "Chime timer ``timerName`` fire"
153 | );
154 | context.complete();
155 | }
156 | }
157 | else {
158 | context.fail(msg);
159 | context.complete();
160 | }
161 | };
162 | }
163 |
164 |
165 | test shared void generateTimerID(AsyncTestContext context) {
166 | eventBus.send (
167 | chime,
168 | JsonObject {
169 | Chime.key.operation -> Chime.operation.create,
170 | Chime.key.maxCount -> 1,
171 | Chime.key.description -> JsonObject {
172 | Chime.key.type -> Chime.type.interval,
173 | Chime.key.delay -> 1
174 | }
175 | },
176 | (Throwable|Message msg) {
177 | if (is Throwable msg) {
178 | context.fail(msg, "Automatic timer ID generation");
179 | context.complete();
180 | }
181 | else {
182 | if (exists resp = msg.body()) {
183 | context.assertThat (
184 | resp[Chime.key.name], PassType(NotEmpty()), "", true
185 | );
186 | context.complete();
187 | }
188 | else {
189 | context.fail(Exception("Chime rejects to setup timer"));
190 | context.complete();
191 | }
192 | }
193 | }
194 | );
195 | }
196 |
197 | test shared void intervalTimer(AsyncTestContext context) {
198 |
199 | Integer timerDelay = 1;
200 |
201 | eventBus.consumer (
202 | interval,
203 | timerValidation(interval, timerDelay, 3, context)
204 | );
205 |
206 | eventBus.send (
207 | chime,
208 | JsonObject {
209 | Chime.key.operation -> Chime.operation.create,
210 | Chime.key.name -> interval,
211 | Chime.key.maxCount -> 3,
212 | Chime.key.description -> JsonObject {
213 | Chime.key.type -> Chime.type.interval,
214 | Chime.key.delay -> timerDelay
215 | }
216 | },
217 | (Throwable|Message msg) {
218 | if (is Throwable msg) {
219 | context.fail(msg, "Interval timer setup");
220 | context.complete();
221 | }
222 | else {
223 | if (!msg.body() exists) {
224 | context.fail(Exception("Chime rejects to setup interval timer"), "Interval timer setup");
225 | context.complete();
226 | }
227 | }
228 | }
229 | );
230 |
231 | }
232 |
233 | test shared void cronTimer(AsyncTestContext context) {
234 |
235 | eventBus.consumer (
236 | cron,
237 | timerValidation(cron, 1, 3, context)
238 | );
239 |
240 | eventBus.send (
241 | chime,
242 | JsonObject {
243 | Chime.key.operation -> Chime.operation.create,
244 | Chime.key.name -> cron,
245 | Chime.key.maxCount -> 3,
246 | Chime.key.description -> JsonObject {
247 | Chime.key.type -> Chime.type.cron,
248 | Chime.date.seconds -> "0-59",
249 | Chime.date.minutes -> "*",
250 | Chime.date.hours -> "0-23",
251 | Chime.date.daysOfMonth -> "1-31",
252 | Chime.date.months -> "*",
253 | Chime.date.daysOfWeek -> "*",
254 | Chime.date.years -> "2015-2019"
255 | }
256 | },
257 | (Throwable|Message msg) {
258 | if (is Throwable msg) {
259 | context.fail(msg, "Cron timer setup");
260 | context.complete();
261 | }
262 | else {
263 | if (!msg.body() exists) {
264 | context.fail(Exception("Chime rejects to setup cron timer"), "Cron timer setup");
265 | context.complete();
266 | }
267 | }
268 | }
269 | );
270 |
271 | }
272 |
273 | test shared void unionTimer(AsyncTestContext context) {
274 |
275 | eventBus.consumer (
276 | union,
277 | timerValidation(union, 1, 3, context)
278 | );
279 |
280 | eventBus.send (
281 | chime,
282 | JsonObject {
283 | Chime.key.operation -> Chime.operation.create,
284 | Chime.key.name -> union,
285 | Chime.key.maxCount -> 3,
286 | Chime.key.description -> JsonObject {
287 | Chime.key.type -> Chime.type.union,
288 | Chime.key.timers -> JsonArray {
289 | JsonObject {
290 | Chime.key.type -> Chime.type.cron,
291 | Chime.date.seconds -> "0-59/3",
292 | Chime.date.minutes -> "*",
293 | Chime.date.hours -> "*",
294 | Chime.date.daysOfMonth -> "*",
295 | Chime.date.months -> "*"
296 | },
297 | JsonObject {
298 | Chime.key.type -> Chime.type.cron,
299 | Chime.date.seconds -> "1-59/3",
300 | Chime.date.minutes -> "*",
301 | Chime.date.hours -> "*",
302 | Chime.date.daysOfMonth -> "*",
303 | Chime.date.months -> "*"
304 | },
305 | JsonObject {
306 | Chime.key.type -> Chime.type.cron,
307 | Chime.date.seconds -> "2-59/3",
308 | Chime.date.minutes -> "*",
309 | Chime.date.hours -> "*",
310 | Chime.date.daysOfMonth -> "*",
311 | Chime.date.months -> "*"
312 | }
313 | }
314 | }
315 | },
316 | (Throwable|Message msg) {
317 | if (is Throwable msg) {
318 | context.fail(msg, "Union timer setup");
319 | context.complete();
320 | }
321 | else {
322 | if (!msg.body() exists) {
323 | context.fail(Exception("Chime rejects to setup union timer"), "Union timer setup");
324 | context.complete();
325 | }
326 | }
327 | }
328 | );
329 |
330 | }
331 |
332 | }
333 |
--------------------------------------------------------------------------------
/test/herd/test/schedule/chime/module.ceylon:
--------------------------------------------------------------------------------
1 | import herd.asynctest {
2 |
3 | async
4 | }
5 |
6 |
7 | "_Chime_ unit testing."
8 | since("0.2.0") by("Lis")
9 | native("jvm") async
10 | module herd.test.schedule.chime "0.3.0" {
11 | import herd.schedule.chime "0.3.0";
12 | shared import ceylon.test "1.3.3.1";
13 | shared import herd.asynctest "0.7.1";
14 | shared import io.vertx.ceylon.core "3.4.2";
15 | }
16 |
--------------------------------------------------------------------------------
/test/herd/test/schedule/chime/package.ceylon:
--------------------------------------------------------------------------------
1 | shared package herd.test.schedule.chime;
2 |
--------------------------------------------------------------------------------