[scheduler] cleaned up and extended scheduler support

- Added additional user friendly schedules
 - `hourly` - a simple to configure schedule that will fire every hour on one or more specific minutes in the hour
 - `daily` - a simple to configure schedule that will fire every day on one or more specific times in the day
 - `weekly` - a simple to configure schedule that will fire every week on one or more specific days + times in the week
 - `monthly` - a simple to configure schedule that will fire every month on one or more specific days + times in the month
 - `yearly` - a simple to configure schedule that will fire every year on one or more specific months + days + times in the year
 - `interval` - a simple interval based schedule that will fire every fixed configurable interval (supported units are: seconds, minutes, hours, days and weeks)

- Added unit tests to all the schedules and the schedule registry
- Introduced `Scheduler` as an interface and `InternalScheduler` for the quartz implementation. This will help unit testing other dependent services
- `Scheduler` is now independent of `Alert`. It works with `Job` constructs (`Alert` now implements a `Job`).
- Introduced `SchedulerMock` as a simple `Scheduler` implementation that can be used for unit tests - enables manual triggering of jobs.

- introduced `@Slow` test annotation support in the `pom.xml`

Original commit: elastic/x-pack-elasticsearch@94a8f5ddea
This commit is contained in:
uboness 2015-02-23 13:28:43 +01:00
parent 59f0883721
commit 89b7d085e1
34 changed files with 4303 additions and 196 deletions

View File

@ -7,6 +7,7 @@ package org.elasticsearch.alerts;
import org.elasticsearch.alerts.actions.ActionRegistry;
import org.elasticsearch.alerts.actions.Actions;
import org.elasticsearch.alerts.scheduler.Scheduler;
import org.elasticsearch.alerts.scheduler.schedule.Schedule;
import org.elasticsearch.alerts.scheduler.schedule.ScheduleRegistry;
import org.elasticsearch.alerts.throttle.AlertThrottler;
@ -37,7 +38,7 @@ import java.util.Map;
import static org.elasticsearch.alerts.support.AlertsDateUtils.*;
public class Alert implements ToXContent {
public class Alert implements Scheduler.Job, ToXContent {
private final String name;
private final Schedule schedule;
@ -366,8 +367,6 @@ public class Alert implements ToXContent {
return false;
}
@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeLong(version);

View File

@ -147,7 +147,7 @@ public class AlertsService extends AbstractComponent {
try {
AlertsStore.AlertPut result = alertsStore.putAlert(name, alertSource);
if (result.previous() == null || !result.previous().schedule().equals(result.current().schedule())) {
scheduler.schedule(result.current());
scheduler.add(result.current());
}
return result.indexResponse();
} finally {

View File

@ -7,17 +7,17 @@ package org.elasticsearch.alerts.scheduler;
import org.quartz.*;
public class FireAlertJob implements Job {
public class FireAlertQuartzJob implements Job {
static final String SCHEDULER_KEY = "scheduler";
public FireAlertJob() {
public FireAlertQuartzJob() {
}
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
String alertName = jobExecutionContext.getJobDetail().getKey().getName();
Scheduler scheduler = (Scheduler) jobExecutionContext.getJobDetail().getJobDataMap().get(SCHEDULER_KEY);
InternalScheduler scheduler = (InternalScheduler) jobExecutionContext.getJobDetail().getJobDataMap().get(SCHEDULER_KEY);
scheduler.notifyListeners(alertName, jobExecutionContext);
}
@ -25,8 +25,8 @@ public class FireAlertJob implements Job {
return new JobKey(alertName);
}
static JobDetail jobDetail(String alertName, Scheduler scheduler) {
JobDetail job = JobBuilder.newJob(FireAlertJob.class).withIdentity(alertName).build();
static JobDetail jobDetail(String alertName, InternalScheduler scheduler) {
JobDetail job = JobBuilder.newJob(FireAlertQuartzJob.class).withIdentity(alertName).build();
job.getJobDataMap().put("scheduler", scheduler);
return job;
}

View File

@ -0,0 +1,191 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.scheduler;
import org.elasticsearch.alerts.AlertsPlugin;
import org.elasticsearch.alerts.AlertsSettingsException;
import org.elasticsearch.alerts.scheduler.schedule.CronnableSchedule;
import org.elasticsearch.alerts.scheduler.schedule.IntervalSchedule;
import org.elasticsearch.alerts.scheduler.schedule.Schedule;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.joda.time.DateTime;
import org.elasticsearch.common.joda.time.DateTimeZone;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.EsThreadPoolExecutor;
import org.elasticsearch.threadpool.ThreadPool;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.simpl.SimpleJobFactory;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import static org.elasticsearch.alerts.scheduler.FireAlertQuartzJob.jobDetail;
public class InternalScheduler extends AbstractComponent implements Scheduler {
// Not happy about it, but otherwise we're stuck with Quartz's SimpleThreadPool
private volatile static ThreadPool threadPool;
private volatile org.quartz.Scheduler scheduler;
private List<Listener> listeners;
private final DateTimeZone defaultTimeZone;
@Inject
public InternalScheduler(Settings settings, ThreadPool threadPool) {
super(settings);
this.listeners = new CopyOnWriteArrayList<>();
InternalScheduler.threadPool = threadPool;
String timeZoneStr = componentSettings.get("time_zone", "UTC");
try {
this.defaultTimeZone = DateTimeZone.forID(timeZoneStr);
} catch (IllegalArgumentException iae) {
throw new AlertsSettingsException("unrecognized time zone setting [" + timeZoneStr + "]", iae);
}
}
@Override
public synchronized void start(Collection<? extends Job> jobs) {
try {
logger.info("Starting scheduler");
// Can't start a scheduler that has been shutdown, so we need to re-create each time start() is invoked
Properties properties = new Properties();
properties.setProperty("org.quartz.threadPool.class", AlertQuartzThreadPool.class.getName());
properties.setProperty(StdSchedulerFactory.PROP_SCHED_SKIP_UPDATE_CHECK, "true");
properties.setProperty(StdSchedulerFactory.PROP_SCHED_INTERRUPT_JOBS_ON_SHUTDOWN, "true");
properties.setProperty(StdSchedulerFactory.PROP_SCHED_INTERRUPT_JOBS_ON_SHUTDOWN_WITH_WAIT, "true");
SchedulerFactory schFactory = new StdSchedulerFactory(properties);
scheduler = schFactory.getScheduler();
scheduler.setJobFactory(new SimpleJobFactory());
Map<JobDetail, Set<? extends Trigger>> quartzJobs = new HashMap<>();
for (Job alert : jobs) {
quartzJobs.put(jobDetail(alert.name(), this), createTrigger(alert.schedule(), defaultTimeZone));
}
scheduler.scheduleJobs(quartzJobs, false);
scheduler.start();
} catch (org.quartz.SchedulerException se) {
logger.error("Failed to start quartz scheduler", se);
}
}
public synchronized void stop() {
try {
org.quartz.Scheduler scheduler = this.scheduler;
if (scheduler != null) {
logger.info("Stopping scheduler...");
scheduler.shutdown(true);
this.scheduler = null;
logger.info("Stopped scheduler");
}
} catch (org.quartz.SchedulerException se){
logger.error("Failed to stop quartz scheduler", se);
}
}
@Override
public void addListener(Listener listener) {
listeners.add(listener);
}
void notifyListeners(String alertName, JobExecutionContext ctx) {
DateTime scheduledTime = new DateTime(ctx.getScheduledFireTime());
DateTime fireTime = new DateTime(ctx.getFireTime());
for (Listener listener : listeners) {
listener.fire(alertName, scheduledTime, fireTime);
}
}
/**
* Schedules the given alert
*/
public void add(Job job) {
try {
logger.trace("scheduling [{}] with schedule [{}]", job.name(), job.schedule());
scheduler.scheduleJob(jobDetail(job.name(), this), createTrigger(job.schedule(), defaultTimeZone), true);
} catch (org.quartz.SchedulerException se) {
logger.error("Failed to schedule job",se);
throw new SchedulerException("Failed to schedule job", se);
}
}
public boolean remove(String jobName) {
try {
return scheduler.deleteJob(new JobKey(jobName));
} catch (org.quartz.SchedulerException se){
throw new SchedulerException("Failed to remove [" + jobName + "] from the scheduler", se);
}
}
static Set<Trigger> createTrigger(Schedule schedule, DateTimeZone timeZone) {
HashSet<Trigger> triggers = new HashSet<>();
if (schedule instanceof CronnableSchedule) {
for (String cron : ((CronnableSchedule) schedule).crons()) {
triggers.add(TriggerBuilder.newTrigger()
.withSchedule(CronScheduleBuilder.cronSchedule(cron).inTimeZone(timeZone.toTimeZone()))
.startNow()
.build());
}
} else {
// must be interval schedule
IntervalSchedule.Interval interval = ((IntervalSchedule) schedule).interval();
triggers.add(TriggerBuilder.newTrigger().withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds((int) interval.seconds())
.repeatForever())
.startNow()
.build());
}
return triggers;
}
// This Quartz thread pool will always accept. On this thread we will only index an alert action and add it to the work queue
public static final class AlertQuartzThreadPool implements org.quartz.spi.ThreadPool {
private final EsThreadPoolExecutor executor;
public AlertQuartzThreadPool() {
this.executor = (EsThreadPoolExecutor) threadPool.executor(AlertsPlugin.SCHEDULER_THREAD_POOL_NAME);
}
@Override
public boolean runInThread(Runnable runnable) {
executor.execute(runnable);
return true;
}
@Override
public int blockForAvailableThreads() {
return 1;
}
@Override
public void initialize() throws SchedulerConfigException {
}
@Override
public void shutdown(boolean waitForJobsToComplete) {
}
@Override
public int getPoolSize() {
return 1;
}
@Override
public void setInstanceId(String schedInstId) {
}
@Override
public void setInstanceName(String schedName) {
}
}
}

View File

@ -5,173 +5,49 @@
*/
package org.elasticsearch.alerts.scheduler;
import org.elasticsearch.alerts.Alert;
import org.elasticsearch.alerts.AlertsPlugin;
import org.elasticsearch.alerts.scheduler.schedule.Schedule;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.joda.time.DateTime;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.EsThreadPoolExecutor;
import org.elasticsearch.threadpool.ThreadPool;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.simpl.SimpleJobFactory;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.Collection;
import static org.elasticsearch.alerts.scheduler.FireAlertJob.jobDetail;
public class Scheduler extends AbstractComponent {
// Not happy about it, but otherwise we're stuck with Quartz's SimpleThreadPool
private volatile static ThreadPool threadPool;
private volatile org.quartz.Scheduler scheduler;
private List<Listener> listeners;
@Inject
public Scheduler(Settings settings, ThreadPool threadPool) {
super(settings);
this.listeners = new CopyOnWriteArrayList<>();
Scheduler.threadPool = threadPool;
}
/**
*
*/
public interface Scheduler {
/**
* Starts the scheduler and schedules the specified alerts before returning.
*
* Both the start and stop are synchronized to avoid that scheduler gets stopped while previously stored alerts
* are being loaded.
* Starts the scheduler and schedules the specified jobs before returning.
*/
public synchronized void start(Collection<Alert> alerts) {
try {
logger.info("Starting scheduler");
// Can't start a scheduler that has been shutdown, so we need to re-create each time start() is invoked
Properties properties = new Properties();
properties.setProperty("org.quartz.threadPool.class", AlertQuartzThreadPool.class.getName());
properties.setProperty(StdSchedulerFactory.PROP_SCHED_SKIP_UPDATE_CHECK, "true");
properties.setProperty(StdSchedulerFactory.PROP_SCHED_INTERRUPT_JOBS_ON_SHUTDOWN, "true");
properties.setProperty(StdSchedulerFactory.PROP_SCHED_INTERRUPT_JOBS_ON_SHUTDOWN_WITH_WAIT, "true");
SchedulerFactory schFactory = new StdSchedulerFactory(properties);
scheduler = schFactory.getScheduler();
scheduler.setJobFactory(new SimpleJobFactory());
Map<JobDetail, Set<? extends Trigger>> jobs = new HashMap<>();
for (Alert alert : alerts) {
jobs.put(jobDetail(alert.name(), this), createTrigger(alert.schedule()));
}
scheduler.scheduleJobs(jobs, false);
scheduler.start();
} catch (org.quartz.SchedulerException se) {
logger.error("Failed to start quartz scheduler", se);
}
}
void start(Collection<? extends Job> jobs);
/**
* Stops the scheduler.
*/
public synchronized void stop() {
try {
org.quartz.Scheduler scheduler = this.scheduler;
if (scheduler != null) {
logger.info("Stopping scheduler...");
scheduler.shutdown(true);
this.scheduler = null;
logger.info("Stopped scheduler");
}
} catch (org.quartz.SchedulerException se){
logger.error("Failed to stop quartz scheduler", se);
}
}
public void addListener(Listener listener) {
listeners.add(listener);
}
void notifyListeners(String alertName, JobExecutionContext ctx) {
DateTime scheduledTime = new DateTime(ctx.getScheduledFireTime());
DateTime fireTime = new DateTime(ctx.getFireTime());
for (Listener listener : listeners) {
listener.fire(alertName, scheduledTime, fireTime);
}
}
void stop();
/**
* Schedules the given alert
* Adds and schedules the give job
*/
public void schedule(Alert alert) {
try {
logger.trace("scheduling [{}] with schedule [{}]", alert.name(), alert.schedule());
scheduler.scheduleJob(jobDetail(alert.name(), this), createTrigger(alert.schedule()), true);
} catch (org.quartz.SchedulerException se) {
logger.error("Failed to schedule job",se);
throw new SchedulerException("Failed to schedule job", se);
}
}
void add(Job job);
public boolean remove(String alertName) {
try {
return scheduler.deleteJob(new JobKey(alertName));
} catch (org.quartz.SchedulerException se){
throw new SchedulerException("Failed to remove [" + alertName + "] from the scheduler", se);
}
}
/**
* Removes the scheduled job that is associated with the given name
*/
boolean remove(String jobName);
static Set<CronTrigger> createTrigger(Schedule schedule) {
return new HashSet<>(Arrays.asList(
TriggerBuilder.newTrigger().withSchedule(CronScheduleBuilder.cronSchedule(schedule.cron())).build()
));
}
// This Quartz thread pool will always accept. On this thread we will only index an alert action and add it to the work queue
public static final class AlertQuartzThreadPool implements org.quartz.spi.ThreadPool {
private final EsThreadPoolExecutor executor;
public AlertQuartzThreadPool() {
this.executor = (EsThreadPoolExecutor) threadPool.executor(AlertsPlugin.SCHEDULER_THREAD_POOL_NAME);
}
@Override
public boolean runInThread(Runnable runnable) {
executor.execute(runnable);
return true;
}
@Override
public int blockForAvailableThreads() {
return 1;
}
@Override
public void initialize() throws SchedulerConfigException {
}
@Override
public void shutdown(boolean waitForJobsToComplete) {
}
@Override
public int getPoolSize() {
return 1;
}
@Override
public void setInstanceId(String schedInstId) {
}
@Override
public void setInstanceName(String schedName) {
}
}
void addListener(Listener listener);
public static interface Listener {
void fire(String alertName, DateTime scheduledFireTime, DateTime fireTime);
void fire(String jobName, DateTime scheduledFireTime, DateTime fireTime);
}
public static interface Job {
String name();
Schedule schedule();
}
}

View File

@ -5,9 +5,7 @@
*/
package org.elasticsearch.alerts.scheduler;
import org.elasticsearch.alerts.scheduler.schedule.CronSchedule;
import org.elasticsearch.alerts.scheduler.schedule.Schedule;
import org.elasticsearch.alerts.scheduler.schedule.ScheduleRegistry;
import org.elasticsearch.alerts.scheduler.schedule.*;
import org.elasticsearch.common.inject.AbstractModule;
import org.elasticsearch.common.inject.multibindings.MapBinder;
@ -29,8 +27,20 @@ public class SchedulerModule extends AbstractModule {
protected void configure() {
MapBinder<String, Schedule.Parser> mbinder = MapBinder.newMapBinder(binder(), String.class, Schedule.Parser.class);
bind(IntervalSchedule.Parser.class).asEagerSingleton();
mbinder.addBinding(IntervalSchedule.TYPE).to(IntervalSchedule.Parser.class);
bind(CronSchedule.Parser.class).asEagerSingleton();
mbinder.addBinding(CronSchedule.TYPE).to(CronSchedule.Parser.class);
bind(HourlySchedule.Parser.class).asEagerSingleton();
mbinder.addBinding(HourlySchedule.TYPE).to(HourlySchedule.Parser.class);
bind(DailySchedule.Parser.class).asEagerSingleton();
mbinder.addBinding(DailySchedule.TYPE).to(DailySchedule.Parser.class);
bind(WeeklySchedule.Parser.class).asEagerSingleton();
mbinder.addBinding(WeeklySchedule.TYPE).to(WeeklySchedule.Parser.class);
bind(MonthlySchedule.Parser.class).asEagerSingleton();
mbinder.addBinding(MonthlySchedule.TYPE).to(MonthlySchedule.Parser.class);
bind(YearlySchedule.Parser.class).asEagerSingleton();
mbinder.addBinding(YearlySchedule.TYPE).to(YearlySchedule.Parser.class);
for (Map.Entry<String, Class<? extends Schedule.Parser>> entry : parsers.entrySet()) {
bind(entry.getValue()).asEagerSingleton();
@ -38,6 +48,6 @@ public class SchedulerModule extends AbstractModule {
}
bind(ScheduleRegistry.class).asEagerSingleton();
bind(Scheduler.class).asEagerSingleton();
bind(Scheduler.class).to(InternalScheduler.class).asEagerSingleton();
}
}

View File

@ -5,22 +5,26 @@
*/
package org.elasticsearch.alerts.scheduler.schedule;
import org.elasticsearch.alerts.AlertsSettingsException;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.quartz.CronExpression;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
/**
*
*/
public class CronSchedule implements Schedule {
public class CronSchedule extends CronnableSchedule {
public static final String TYPE = "cron";
private final String cron;
public CronSchedule(String cron) {
this.cron = cron;
public CronSchedule(String... crons) {
super(crons);
validate(crons);
}
@Override
@ -29,13 +33,18 @@ public class CronSchedule implements Schedule {
}
@Override
public String cron() {
return cron;
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return crons.length == 1 ? builder.value(crons[0]) : builder.value(crons);
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return builder.value(cron);
static void validate(String... crons) {
for (String cron :crons) {
try {
CronExpression.validateExpression(cron);
} catch (ParseException pe) {
throw new ValidationException(cron, pe);
}
}
}
public static class Parser implements Schedule.Parser<CronSchedule> {
@ -47,26 +56,43 @@ public class CronSchedule implements Schedule {
@Override
public CronSchedule parse(XContentParser parser) throws IOException {
assert parser.currentToken() == XContentParser.Token.VALUE_STRING : "expecting a string value with cron expression";
String cron = parser.text();
return new CronSchedule(cron);
try {
XContentParser.Token token = parser.currentToken();
if (token == XContentParser.Token.VALUE_STRING) {
return new CronSchedule(parser.text());
} else if (token == XContentParser.Token.START_ARRAY) {
List<String> crons = new ArrayList<>();
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
switch (token) {
case VALUE_STRING:
crons.add(parser.text());
break;
default:
throw new AlertsSettingsException("could not parse [cron] schedule. expected a string value in the cron array but found [" + token + "]");
}
}
if (crons.isEmpty()) {
throw new AlertsSettingsException("could not parse [cron] schedule. no cron expression found in cron array");
}
return new CronSchedule(crons.toArray(new String[crons.size()]));
} else {
throw new AlertsSettingsException("could not parse [cron] schedule. expected either a cron string value or an array of cron string values, but found [" + token + "]");
}
} catch (ValidationException ve) {
throw new AlertsSettingsException("could not parse [cron] schedule. invalid cron expression [" + ve.expression + "]", ve);
}
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
public static class ValidationException extends AlertsSettingsException {
CronSchedule that = (CronSchedule) o;
private String expression;
if (cron != null ? !cron.equals(that.cron) : that.cron != null) return false;
return true;
public ValidationException(String expression, ParseException cause) {
super("invalid cron expression [" + expression + "]. " + cause.getMessage());
this.expression = expression;
}
@Override
public int hashCode() {
return cron != null ? cron.hashCode() : 0;
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.scheduler.schedule;
import java.util.Arrays;
import java.util.Objects;
/**
*
*/
public abstract class CronnableSchedule implements Schedule {
protected final String[] crons;
public CronnableSchedule(String... crons) {
this.crons = crons;
Arrays.sort(crons);
}
public String[] crons() {
return crons;
}
@Override
public int hashCode() {
return Objects.hash((Object[]) crons);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
final CronnableSchedule other = (CronnableSchedule) obj;
return Objects.deepEquals(this.crons, other.crons);
}
}

View File

@ -0,0 +1,147 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.scheduler.schedule;
import org.elasticsearch.alerts.AlertsSettingsException;
import org.elasticsearch.alerts.scheduler.schedule.support.DayTimes;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
*
*/
public class DailySchedule extends CronnableSchedule {
public static final String TYPE = "daily";
public static final DayTimes[] DEFAULT_TIMES = new DayTimes[] { DayTimes.MIDNIGHT };
private final DayTimes[] times;
DailySchedule() {
this(DEFAULT_TIMES);
}
DailySchedule(DayTimes... times) {
super(crons(times));
this.times = times;
}
@Override
public String type() {
return TYPE;
}
public DayTimes[] times() {
return times;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
if (params.paramAsBoolean("normalize", false) && times.length == 1) {
builder.field(Parser.AT_FIELD.getPreferredName(), times[0]);
} else {
builder.field(Parser.AT_FIELD.getPreferredName(), (Object[]) times);
}
return builder.endObject();
}
public static Builder builder() {
return new Builder();
}
static String[] crons(DayTimes[] times) {
assert times.length > 0 : "at least one time must be defined";
List<String> crons = new ArrayList<>(times.length);
for (DayTimes time : times) {
crons.add(time.cron());
}
return crons.toArray(new String[crons.size()]);
}
public static class Parser implements Schedule.Parser<DailySchedule> {
static final ParseField AT_FIELD = new ParseField("at");
@Override
public String type() {
return TYPE;
}
@Override
public DailySchedule parse(XContentParser parser) throws IOException {
List<DayTimes> times = new ArrayList<>();
String currentFieldName = null;
XContentParser.Token token;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if (AT_FIELD.match(currentFieldName)) {
if (token != XContentParser.Token.START_ARRAY) {
try {
times.add(DayTimes.parse(parser, token));
} catch (DayTimes.ParseException pe) {
throw new AlertsSettingsException("could not parse [daily] schedule. invalid time value for field [at] - [" + token + "]", pe);
}
} else {
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
try {
times.add(DayTimes.parse(parser, token));
} catch (DayTimes.ParseException pe) {
throw new AlertsSettingsException("could not parse [daily] schedule. invalid time value for field [at] - [" + token + "]", pe);
}
}
}
} else {
throw new AlertsSettingsException("could not parse [daily] schedule. unexpected field [" + currentFieldName + "]");
}
}
return times.isEmpty() ? new DailySchedule() : new DailySchedule(times.toArray(new DayTimes[times.size()]));
}
}
public static class Builder {
private Set<DayTimes> times = new HashSet<>();
private Builder() {
}
public Builder at(int hour, int minute) {
times.add(new DayTimes(hour, minute));
return this;
}
public Builder atRoundHour(int... hours) {
times.add(new DayTimes(hours, new int[] { 0 }));
return this;
}
public Builder atNoon() {
times.add(DayTimes.NOON);
return this;
}
public Builder atMidnight() {
times.add(DayTimes.MIDNIGHT);
return this;
}
public DailySchedule build() {
return times.isEmpty() ? new DailySchedule() : new DailySchedule(times.toArray(new DayTimes[times.size()]));
}
}
}

View File

@ -0,0 +1,148 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.scheduler.schedule;
import org.elasticsearch.alerts.AlertsSettingsException;
import org.elasticsearch.alerts.scheduler.schedule.support.DayTimes;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.primitives.Ints;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
*
*/
public class HourlySchedule extends CronnableSchedule {
public static final String TYPE = "hourly";
public static final int[] DEFAULT_MINUTES = new int[] { 0 };
private final int[] minutes;
HourlySchedule() {
this(DEFAULT_MINUTES);
}
HourlySchedule(int... minutes) {
super(cron(minutes));
this.minutes = minutes;
}
@Override
public String type() {
return TYPE;
}
public int[] minutes() {
return minutes;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
if (params.paramAsBoolean("normalize", false) && minutes.length == 1) {
builder.field(Parser.MINUTE_FIELD.getPreferredName(), minutes[0]);
} else {
builder.field(Parser.MINUTE_FIELD.getPreferredName(), minutes);
}
return builder.endObject();
}
public static Builder builder() {
return new Builder();
}
static String cron(int[] minutes) {
assert minutes.length > 0 : "at least one minute must be defined";
StringBuilder sb = new StringBuilder("0 ");
for (int i = 0; i < minutes.length; i++) {
if (i != 0) {
sb.append(",");
}
if (!validMinute(minutes[i])) {
throw new AlertsSettingsException("invalid hourly minute [" + minutes[i] + "]. minute must be between 0 and 59 incl.");
}
sb.append(minutes[i]);
}
return sb.append(" * * * ?").toString();
}
static boolean validMinute(int minute) {
return minute >= 0 && minute < 60;
}
public static class Parser implements Schedule.Parser<HourlySchedule> {
static final ParseField MINUTE_FIELD = new ParseField("minute");
@Override
public String type() {
return TYPE;
}
@Override
public HourlySchedule parse(XContentParser parser) throws IOException {
List<Integer> minutes = new ArrayList<>();
String currentFieldName = null;
XContentParser.Token token;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if (MINUTE_FIELD.match(currentFieldName)) {
if (token.isValue()) {
try {
minutes.add(DayTimes.parseMinuteValue(parser, token));
} catch (DayTimes.ParseException pe) {
throw new AlertsSettingsException("could not parse [hourly] schedule. invalid value for [minute]", pe);
}
} else if (token == XContentParser.Token.START_ARRAY) {
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
try {
minutes.add(DayTimes.parseMinuteValue(parser, token));
} catch (DayTimes.ParseException pe) {
throw new AlertsSettingsException("could not parse [hourly] schedule. invalid value for [minute]", pe);
}
}
} else {
throw new AlertsSettingsException("could not parse [hourly] schedule. invalid minute value. expected either string/value or an array of string/number values, but found [" + token + "]");
}
} else {
throw new AlertsSettingsException("could not parse [hourly] schedule. unexpected field [" + currentFieldName + "]");
}
}
return minutes.isEmpty() ? new HourlySchedule() : new HourlySchedule(Ints.toArray(minutes));
}
}
public static class Builder {
private Set<Integer> minutes = new HashSet<>();
private Builder() {
}
public Builder minutes(int... minutes) {
for (int minute : minutes) {
this.minutes.add(minute);
}
return this;
}
public HourlySchedule build() {
return minutes.isEmpty() ? new HourlySchedule() : new HourlySchedule(Ints.toArray(minutes));
}
}
}

View File

@ -0,0 +1,187 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.scheduler.schedule;
import org.elasticsearch.alerts.AlertsSettingsException;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import java.io.IOException;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
/**
*
*/
public class IntervalSchedule implements Schedule {
public static final String TYPE = "interval";
private final Interval interval;
public IntervalSchedule(Interval interval) {
this.interval = interval;
}
@Override
public String type() {
return TYPE;
}
public Interval interval() {
return interval;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return interval.toXContent(builder, params);
}
@Override
public String toString() {
return interval.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
IntervalSchedule schedule = (IntervalSchedule) o;
if (!interval.equals(schedule.interval)) return false;
return true;
}
@Override
public int hashCode() {
return interval.hashCode();
}
public static class Parser implements Schedule.Parser<IntervalSchedule> {
@Override
public String type() {
return TYPE;
}
@Override
public IntervalSchedule parse(XContentParser parser) throws IOException {
XContentParser.Token token = parser.currentToken();
if (token == XContentParser.Token.VALUE_NUMBER) {
return new IntervalSchedule(Interval.seconds(parser.longValue()));
}
if (token == XContentParser.Token.VALUE_STRING) {
String value = parser.text();
return new IntervalSchedule(Interval.parse(value));
}
throw new AlertsSettingsException("could not parse [interval] schedule. expected either a numeric value " +
"(millis) or a string value representing time value (e.g. '5s'), but found [" + token + "]");
}
}
/**
* Represents a time interval. Ideally we would have used TimeValue here, but we don't because:
* 1. We should limit the time values that the user can configure (we don't want to support nanos & millis
* 2. TimeValue formatting & parsing is inconsistent (it doesn't format to a value that it can parse)
* 3. The equals of TimeValue is odd - it will only equate two time values that have the exact same unit & duration,
* this interval on the other hand, equates based on the millis value.
* 4. We have the advantage of making this interval construct a ToXContent
*/
public static class Interval implements ToXContent {
public static enum Unit {
SECONDS(TimeUnit.SECONDS.toMillis(1), "s"),
MINUTES(TimeUnit.MINUTES.toMillis(1), "m"),
HOURS(TimeUnit.HOURS.toMillis(1), "h"),
DAYS(TimeUnit.DAYS.toMillis(1), "d"),
WEEK(TimeUnit.DAYS.toMillis(7), "w");
private final String suffix;
private final long millis;
private Unit(long millis, String suffix) {
this.millis = millis;
this.suffix = suffix;
}
public long millis(long duration) {
return duration * millis;
}
public long parse(String value) {
assert value.endsWith(suffix);
String num = value.substring(0, value.indexOf(suffix));
try {
return Long.parseLong(num);
} catch (NumberFormatException nfe) {
throw new AlertsSettingsException("could not parse [interval] schedule. could not parse ["
+ num + "] as a " + name().toLowerCase(Locale.ROOT) + " duration");
}
}
public String format(long duration) {
return duration + suffix;
}
}
private final long duration;
private final Unit unit;
public Interval(long duration, Unit unit) {
this.duration = duration;
this.unit = unit;
}
public long seconds() {
return unit.millis(duration) / Unit.SECONDS.millis;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return builder.value(unit.format(duration));
}
@Override
public String toString() {
return unit.format(duration);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Interval interval = (Interval) o;
if (unit.millis(duration) != interval.unit.millis(duration)) return false;
return true;
}
@Override
public int hashCode() {
long millis = unit.millis(duration);
int result = (int) (millis ^ (millis >>> 32));
return result;
}
public static Interval seconds(long duration) {
return new Interval(duration, Unit.SECONDS);
}
public static Interval parse(String value) {
for (Unit unit : Unit.values()) {
if (value.endsWith(unit.suffix)) {
return new Interval(unit.parse(value), unit);
}
}
throw new AlertsSettingsException("could not parse [interval] schedule. unrecognized interval format [" + value + "]");
}
}
}

View File

@ -0,0 +1,123 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.scheduler.schedule;
import org.elasticsearch.alerts.AlertsSettingsException;
import org.elasticsearch.alerts.scheduler.schedule.support.MonthTimes;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
*
*/
public class MonthlySchedule extends CronnableSchedule {
public static final String TYPE = "monthly";
public static final MonthTimes[] DEFAULT_TIMES = new MonthTimes[] { new MonthTimes() };
private final MonthTimes[] times;
MonthlySchedule() {
this(DEFAULT_TIMES);
}
MonthlySchedule(MonthTimes... times) {
super(crons(times));
this.times = times;
}
@Override
public String type() {
return TYPE;
}
public MonthTimes[] times() {
return times;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
if (params.paramAsBoolean("normalize", false) && times.length == 1) {
return builder.value(times[0]);
}
return builder.value(times);
}
public static Builder builder() {
return new Builder();
}
static String[] crons(MonthTimes[] times) {
assert times.length > 0 : "at least one time must be defined";
Set<String> crons = new HashSet<>(times.length);
for (MonthTimes time : times) {
crons.addAll(time.crons());
}
return crons.toArray(new String[crons.size()]);
}
public static class Parser implements Schedule.Parser<MonthlySchedule> {
@Override
public String type() {
return TYPE;
}
@Override
public MonthlySchedule parse(XContentParser parser) throws IOException {
if (parser.currentToken() == XContentParser.Token.START_OBJECT) {
try {
return new MonthlySchedule(MonthTimes.parse(parser, parser.currentToken()));
} catch (MonthTimes.ParseException pe) {
throw new AlertsSettingsException("could not parse [monthly] schedule. invalid month times", pe);
}
}
if (parser.currentToken() == XContentParser.Token.START_ARRAY) {
List<MonthTimes> times = new ArrayList<>();
XContentParser.Token token;
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
try {
times.add(MonthTimes.parse(parser, token));
} catch (MonthTimes.ParseException pe) {
throw new AlertsSettingsException("could not parse [monthly] schedule. invalid month times", pe);
}
}
return times.isEmpty() ? new MonthlySchedule() : new MonthlySchedule(times.toArray(new MonthTimes[times.size()]));
}
throw new AlertsSettingsException("could not parse [monthly] schedule. expected either an object or an array " +
"of objects representing month times, but found [" + parser.currentToken() + "] instead");
}
}
public static class Builder {
private final Set<MonthTimes> times = new HashSet<>();
private Builder() {
}
public Builder time(MonthTimes time) {
times.add(time);
return this;
}
public Builder time(MonthTimes.Builder builder) {
return time(builder.build());
}
public MonthlySchedule build() {
return times.isEmpty() ? new MonthlySchedule() : new MonthlySchedule(times.toArray(new MonthTimes[times.size()]));
}
}
}

View File

@ -17,8 +17,6 @@ public interface Schedule extends ToXContent {
String type();
String cron();
static interface Parser<S extends Schedule> {
String type();

View File

@ -32,13 +32,14 @@ public class ScheduleRegistry {
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
type = parser.currentName();
} else if ((token.isValue() || token == XContentParser.Token.START_OBJECT) && type != null) {
Schedule.Parser scheduleParser = parsers.get(type);
if (scheduleParser == null) {
throw new AlertsSettingsException("unknown schedule type [" + type + "]");
} else if (type != null) {
schedule = parse(type, parser);
} else {
throw new AlertsSettingsException("could not parse schedule. expected a schedule type field, but found [" + token + "]");
}
schedule = scheduleParser.parse(parser);
}
if (schedule == null) {
throw new AlertsSettingsException("could not parse schedule. expected a schedule type field, but no fields were found");
}
return schedule;
}
@ -46,7 +47,7 @@ public class ScheduleRegistry {
public Schedule parse(String type, XContentParser parser) throws IOException {
Schedule.Parser scheduleParser = parsers.get(type);
if (scheduleParser == null) {
throw new AlertsSettingsException("unknown schedule type [" + type + "]");
throw new AlertsSettingsException("could not parse schedule. unknown schedule type [" + type + "]");
}
return scheduleParser.parse(parser);
}

View File

@ -0,0 +1,94 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.scheduler.schedule;
/**
* A static factory for all available schedules.
*/
public class Schedules {
private Schedules() {
}
/**
* Creates an interval schedule. The provided string can have the following format:
* <ul>
* <li>34s</li> - a 34 seconds long interval
* <li>23m</li> - a 23 minutes long interval
* <li>40h</li> - a 40 hours long interval
* <li>63d</li> - a 63 days long interval
* <li>27w</li> - a 27 weeks long interval
* </ul>
*
* @param interval The fixed interval by which the schedule will trigger.
* @return The newly created interval schedule
*/
public static IntervalSchedule interval(String interval) {
return new IntervalSchedule(IntervalSchedule.Interval.parse(interval));
}
/**
* Creates an interval schedule.
*
* @param duration The duration of the interval
* @param unit The unit of the duration (seconds, minutes, hours, days or weeks)
* @return The newly created interval schedule.
*/
public static IntervalSchedule interval(long duration, IntervalSchedule.Interval.Unit unit) {
return new IntervalSchedule(new IntervalSchedule.Interval(duration, unit));
}
/**
* Creates a cron schedule.
*
* @param cronExpressions one or more cron expressions
* @return the newly created cron schedule.
* @throws org.elasticsearch.alerts.scheduler.schedule.CronSchedule.ValidationException if any of the given expression is invalid
*/
public static CronSchedule cron(String... cronExpressions) {
return new CronSchedule(cronExpressions);
}
/**
* Creates an hourly schedule.
*
* @param minutes the minutes within the hour that the schedule should trigger at. values must be
* between 0 and 59 (inclusive).
* @return the newly created hourly schedule
* @throws org.elasticsearch.alerts.AlertsSettingsException if any of the provided minutes are out of valid range
*/
public static HourlySchedule hourly(int... minutes) {
return new HourlySchedule(minutes);
}
/**
* @return A builder for an hourly schedule.
*/
public static HourlySchedule.Builder hourly() {
return HourlySchedule.builder();
}
/**
* @return A builder for a daily schedule.
*/
public static DailySchedule.Builder daily() {
return DailySchedule.builder();
}
/**
* @return A builder for a weekly schedule.
*/
public static WeeklySchedule.Builder weekly() {
return WeeklySchedule.builder();
}
/**
* @return A builder for a monthly schedule.
*/
public static MonthlySchedule.Builder monthly() {
return MonthlySchedule.builder();
}
}

View File

@ -0,0 +1,122 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.scheduler.schedule;
import org.elasticsearch.alerts.AlertsSettingsException;
import org.elasticsearch.alerts.scheduler.schedule.support.WeekTimes;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
*
*/
public class WeeklySchedule extends CronnableSchedule {
public static final String TYPE = "weekly";
public static final WeekTimes[] DEFAULT_TIMES = new WeekTimes[] { new WeekTimes() };
private final WeekTimes[] times;
WeeklySchedule() {
this(DEFAULT_TIMES);
}
WeeklySchedule(WeekTimes... times) {
super(crons(times));
this.times = times;
}
@Override
public String type() {
return TYPE;
}
public WeekTimes[] times() {
return times;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
if (params.paramAsBoolean("normalize", false) && times.length == 1) {
return builder.value(times[0]);
}
return builder.value(times);
}
public static Builder builder() {
return new Builder();
}
static String[] crons(WeekTimes[] times) {
assert times.length > 0 : "at least one time must be defined";
List<String> crons = new ArrayList<>(times.length);
for (WeekTimes time : times) {
crons.addAll(time.crons());
}
return crons.toArray(new String[crons.size()]);
}
public static class Parser implements Schedule.Parser<WeeklySchedule> {
@Override
public String type() {
return TYPE;
}
@Override
public WeeklySchedule parse(XContentParser parser) throws IOException {
if (parser.currentToken() == XContentParser.Token.START_OBJECT) {
try {
return new WeeklySchedule(WeekTimes.parse(parser, parser.currentToken()));
} catch (WeekTimes.ParseException pe) {
throw new AlertsSettingsException("could not parse [weekly] schedule. invalid weekly times", pe);
}
}
if (parser.currentToken() == XContentParser.Token.START_ARRAY) {
List<WeekTimes> times = new ArrayList<>();
XContentParser.Token token;
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
try {
times.add(WeekTimes.parse(parser, token));
} catch (WeekTimes.ParseException pe) {
throw new AlertsSettingsException("could not parse [weekly] schedule. invalid weekly times", pe);
}
}
return times.isEmpty() ? new WeeklySchedule() : new WeeklySchedule(times.toArray(new WeekTimes[times.size()]));
}
throw new AlertsSettingsException("could not parse [weekly] schedule. expected either an object or an array " +
"of objects representing weekly times, but found [" + parser.currentToken() + "] instead");
}
}
public static class Builder {
private final Set<WeekTimes> times = new HashSet<>();
public Builder time(WeekTimes time) {
times.add(time);
return this;
}
public Builder time(WeekTimes.Builder time) {
return time(time.build());
}
public WeeklySchedule build() {
return times.isEmpty() ? new WeeklySchedule() : new WeeklySchedule(times.toArray(new WeekTimes[times.size()]));
}
}
}

View File

@ -0,0 +1,123 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.scheduler.schedule;
import org.elasticsearch.alerts.AlertsSettingsException;
import org.elasticsearch.alerts.scheduler.schedule.support.YearTimes;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
*
*/
public class YearlySchedule extends CronnableSchedule {
public static final String TYPE = "yearly";
public static final YearTimes[] DEFAULT_TIMES = new YearTimes[] { new YearTimes() };
private final YearTimes[] times;
YearlySchedule() {
this(DEFAULT_TIMES);
}
YearlySchedule(YearTimes... times) {
super(crons(times));
this.times = times;
}
@Override
public String type() {
return TYPE;
}
public YearTimes[] times() {
return times;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
if (params.paramAsBoolean("normalize", false) && times.length == 1) {
return builder.value(times[0]);
}
return builder.value(times);
}
public static Builder builder() {
return new Builder();
}
static String[] crons(YearTimes[] times) {
assert times.length > 0 : "at least one time must be defined";
Set<String> crons = new HashSet<>(times.length);
for (YearTimes time : times) {
crons.addAll(time.crons());
}
return crons.toArray(new String[crons.size()]);
}
public static class Parser implements Schedule.Parser<YearlySchedule> {
@Override
public String type() {
return TYPE;
}
@Override
public YearlySchedule parse(XContentParser parser) throws IOException {
if (parser.currentToken() == XContentParser.Token.START_OBJECT) {
try {
return new YearlySchedule(YearTimes.parse(parser, parser.currentToken()));
} catch (YearTimes.ParseException pe) {
throw new AlertsSettingsException("could not parse [yearly] schedule. invalid year times", pe);
}
}
if (parser.currentToken() == XContentParser.Token.START_ARRAY) {
List<YearTimes> times = new ArrayList<>();
XContentParser.Token token;
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
try {
times.add(YearTimes.parse(parser, token));
} catch (YearTimes.ParseException pe) {
throw new AlertsSettingsException("could not parse [yearly] schedule. invalid year times", pe);
}
}
return times.isEmpty() ? new YearlySchedule() : new YearlySchedule(times.toArray(new YearTimes[times.size()]));
}
throw new AlertsSettingsException("could not parse [yearly] schedule. expected either an object or an array " +
"of objects representing year times, but found [" + parser.currentToken() + "] instead");
}
}
public static class Builder {
private final Set<YearTimes> times = new HashSet<>();
private Builder() {
}
public Builder time(YearTimes time) {
times.add(time);
return this;
}
public Builder time(YearTimes.Builder builder) {
return time(builder.build());
}
public YearlySchedule build() {
return times.isEmpty() ? new YearlySchedule() : new YearlySchedule(times.toArray(new YearTimes[times.size()]));
}
}
}

View File

@ -0,0 +1,97 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.scheduler.schedule.support;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import java.io.IOException;
import java.util.EnumSet;
import java.util.Locale;
/**
*
*/
public enum DayOfWeek implements ToXContent {
SUNDAY("SUN"),
MONDAY("MON"),
TUESDAY("TUE"),
WEDNESDAY("WED"),
THURSDAY("THU"),
FRIDAY("FRI"),
SATURDAY("SAT");
private final String cronKey;
private DayOfWeek(String cronKey) {
this.cronKey = cronKey;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return builder.value(name().toLowerCase(Locale.ROOT));
}
public static String cronPart(EnumSet<DayOfWeek> days) {
StringBuilder sb = new StringBuilder();
for (DayOfWeek day : days) {
if (sb.length() != 0) {
sb.append(",");
}
sb.append(day.cronKey);
}
return sb.toString();
}
public static DayOfWeek resolve(int day) {
switch (day) {
case 1: return SUNDAY;
case 2: return MONDAY;
case 3: return TUESDAY;
case 4: return WEDNESDAY;
case 5: return THURSDAY;
case 6: return FRIDAY;
case 7: return SATURDAY;
default:
throw new WeekTimes.ParseException("unknown day of week number [" + day + "]");
}
}
public static DayOfWeek resolve(String day) {
switch (day.toLowerCase(Locale.ROOT)) {
case "1":
case "sun":
case "sunday": return SUNDAY;
case "2":
case "mon":
case "monday": return MONDAY;
case "3":
case "tue":
case "tuesday": return TUESDAY;
case "4":
case "wed":
case "wednesday": return WEDNESDAY;
case "5":
case "thu":
case "thursday": return THURSDAY;
case "6":
case "fri":
case "friday": return FRIDAY;
case "7":
case "sat":
case "saturday": return SATURDAY;
default:
throw new WeekTimes.ParseException("unknown day of week [" + day + "]");
}
}
@Override
public String toString() {
return cronKey;
}
}

View File

@ -0,0 +1,289 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.scheduler.schedule.support;
import org.elasticsearch.alerts.AlertsException;
import org.elasticsearch.alerts.AlertsSettingsException;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.primitives.Ints;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
*
*/
public class DayTimes implements Times {
public static final DayTimes NOON = new DayTimes("noon", new int[] { 12 }, new int[] { 0 });
public static final DayTimes MIDNIGHT = new DayTimes("midnight", new int[] { 0 }, new int[] { 0 });
final int[] hour;
final int[] minute;
final String time;
public DayTimes() {
this(0, 0);
}
public DayTimes(int hour, int minute) {
this(new int[] { hour }, new int[] { minute });
}
public DayTimes(int[] hour, int[] minute) {
this(null, hour, minute);
}
DayTimes(String time, int[] hour, int[] minute) {
this.time = time;
this.hour = hour;
this.minute = minute;
validate();
}
public int[] hour() {
return hour;
}
public int[] minute() {
return minute;
}
public String time() {
return time;
}
public static DayTimes parse(String time) throws ParseException {
if (NOON.time.equals(time)) {
return NOON;
}
if (MIDNIGHT.time.equals(time)) {
return MIDNIGHT;
}
int[] hour;
int[] minute;
int i = time.indexOf(":");
if (i < 0) {
throw new ParseException("could not parse time [" + time + "]. time format must be in the form of hh:mm");
}
if (i == time.length() - 1 || time.indexOf(":", i + 1) >= 0) {
throw new ParseException("could not parse time [" + time + "]. time format must be in the form of hh:mm");
}
String hrStr = time.substring(0, i);
String minStr = time.substring(i + 1);
if (hrStr.length() != 1 && hrStr.length() != 2) {
throw new ParseException("could not parse time [" + time + "]. time format must be in the form of hh:mm");
}
if (minStr.length() != 2) {
throw new ParseException("could not parse time [" + time + "]. time format must be in the form of hh:mm");
}
try {
hour = new int[] { Integer.parseInt(hrStr) };
} catch (NumberFormatException nfe) {
throw new ParseException("could not parse time [" + time + "]. time hour [" + hrStr + "] is not a number ");
}
try {
minute = new int[] { Integer.parseInt(minStr) };
} catch (NumberFormatException nfe) {
throw new ParseException("could not parse time [" + time + "]. time minute [" + minStr + "] is not a number ");
}
return new DayTimes(time, hour, minute);
}
public void validate() {
for (int i = 0; i < hour.length; i++) {
if (!validHour(hour[i])) {
throw new AlertsSettingsException("invalid time [" + this + "]. invalid time hour value [" + hour[i] + "]. time hours must be between 0 and 23 incl.");
}
}
for (int i = 0; i < minute.length; i++) {
if (!validMinute(minute[i])) {
throw new AlertsSettingsException("invalid time [" + this + "]. invalid time minute value [" + minute[i] + "]. time minutes must be between 0 and 59 incl.");
}
}
}
static boolean validHour(int hour) {
return hour >= 0 && hour < 24;
}
static boolean validMinute(int minute) {
return minute >= 0 && minute < 60;
}
public String cron() {
String hrs = Ints.join(",", hour);
String mins = Ints.join(",", minute);
return "0 " + mins + " " + hrs + " * * ?";
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
if (time != null) {
return builder.value(time);
}
return builder.startObject()
.field(HOUR_FIELD.getPreferredName(), hour)
.field(MINUTE_FIELD.getPreferredName(), minute)
.endObject();
}
@Override
public String toString() {
if (time != null) {
return time;
}
StringBuilder sb = new StringBuilder();
for (int h = 0; h < hour.length; h++) {
for (int m = 0; m < minute.length; m++) {
if (sb.length() > 0) {
sb.append(", ");
}
if (hour[h] < 10) {
sb.append("0");
}
sb.append(hour[h]).append(":");
if (minute[m] < 10) {
sb.append("0");
}
sb.append(minute[m]);
}
}
return sb.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DayTimes time = (DayTimes) o;
if (!Arrays.equals(hour, time.hour)) return false;
if (!Arrays.equals(minute, time.minute)) return false;
return true;
}
@Override
public int hashCode() {
int result = Arrays.hashCode(hour);
result = 31 * result + Arrays.hashCode(minute);
return result;
}
public static DayTimes parse(XContentParser parser, XContentParser.Token token) throws IOException, ParseException {
if (token == XContentParser.Token.VALUE_STRING) {
return DayTimes.parse(parser.text());
}
if (token != XContentParser.Token.START_OBJECT) {
throw new ParseException("could not parse time. expected string/number value or an object, but found [" + token + "]");
}
List<Integer> hours = new ArrayList<>();
List<Integer> minutes = new ArrayList<>();
String currentFieldName = null;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if (HOUR_FIELD.match(currentFieldName)) {
if (token.isValue()) {
hours.add(parseHourValue(parser, token));
} else if (token == XContentParser.Token.START_ARRAY) {
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
hours.add(parseHourValue(parser, token));
}
} else {
throw new ParseException("invalid time hour value. expected string/number value or an array of string/number values, but found [" + token + "]");
}
} else if (MINUTE_FIELD.match(currentFieldName)) {
if (token.isValue()) {
minutes.add(parseMinuteValue(parser, token));
} else if (token == XContentParser.Token.START_ARRAY) {
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
minutes.add(parseMinuteValue(parser, token));
}
} else {
throw new ParseException("invalid time minute value. expected string/number value or an array of string/number values, but found [" + token + "]");
}
}
}
if (hours.isEmpty()) {
hours.add(0);
}
if (minutes.isEmpty()) {
minutes.add(0);
}
return new DayTimes(Ints.toArray(hours), Ints.toArray(minutes));
}
public static int parseHourValue(XContentParser parser, XContentParser.Token token) throws IOException, ParseException {
switch (token) {
case VALUE_NUMBER:
int hour = parser.intValue();
if (!DayTimes.validHour(hour)) {
throw new ParseException("invalid time hour value [" + hour + "] (possible values may be between 0 and 23 incl.)");
}
return hour;
case VALUE_STRING:
String value = parser.text();
try {
hour = Integer.valueOf(value);
if (!DayTimes.validHour(hour)) {
throw new ParseException("invalid time hour value [" + hour + "] (possible values may be between 0 and 23 incl.)");
}
return hour;
} catch (NumberFormatException nfe) {
throw new ParseException("invalid time hour value [" + value + "]");
}
default:
throw new ParseException("invalid hour value. expected string/number value, but found [" + token + "]");
}
}
public static int parseMinuteValue(XContentParser parser, XContentParser.Token token) throws IOException, ParseException {
switch (token) {
case VALUE_NUMBER:
int minute = parser.intValue();
if (!DayTimes.validMinute(minute)) {
throw new ParseException("invalid time minute value [" + minute + "] (possible values may be between 0 and 59 incl.)");
}
return minute;
case VALUE_STRING:
String value = parser.text();
try {
minute = Integer.valueOf(value);
if (!DayTimes.validMinute(minute)) {
throw new ParseException("invalid time minute value [" + minute + "] (possible values may be between 0 and 59 incl.)");
}
return minute;
} catch (NumberFormatException nfe) {
throw new ParseException("invalid time minute value [" + value + "]");
}
default:
throw new ParseException("invalid time minute value. expected string/number value, but found [" + token + "]");
}
}
public static class ParseException extends AlertsException {
public ParseException(String msg) {
super(msg);
}
public ParseException(String msg, Throwable cause) {
super(msg, cause);
}
}
}

View File

@ -0,0 +1,123 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.scheduler.schedule.support;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import java.io.IOException;
import java.util.EnumSet;
import java.util.Locale;
/**
*
*/
public enum Month implements ToXContent {
JANUARY("JAN"),
FEBRUARY("FEB"),
MARCH("MAR"),
APRIL("APR"),
MAY("MAY"),
JUNE("JUN"),
JULY("JUL"),
AUGUST("AUG"),
SEPTEMBER("SEP"),
OCTOBER("OCT"),
NOVEMBER("NOV"),
DECEMBER("DEC");
private final String cronKey;
private Month(String cronKey) {
this.cronKey = cronKey;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return builder.value(name().toLowerCase(Locale.ROOT));
}
public static String cronPart(EnumSet<Month> days) {
StringBuilder sb = new StringBuilder();
for (Month day : days) {
if (sb.length() != 0) {
sb.append(",");
}
sb.append(day.cronKey);
}
return sb.toString();
}
public static Month resolve(int month) {
switch (month) {
case 1: return JANUARY;
case 2: return FEBRUARY;
case 3: return MARCH;
case 4: return APRIL;
case 5: return MAY;
case 6: return JUNE;
case 7: return JULY;
case 8: return AUGUST;
case 9: return SEPTEMBER;
case 10: return OCTOBER;
case 11: return NOVEMBER;
case 12: return DECEMBER;
default:
throw new YearTimes.ParseException("unknown month number [" + month + "]");
}
}
public static Month resolve(String day) {
switch (day.toLowerCase(Locale.ROOT)) {
case "1":
case "jan":
case "first":
case "january": return JANUARY;
case "2":
case "feb":
case "february": return FEBRUARY;
case "3":
case "mar":
case "march": return MARCH;
case "4":
case "apr":
case "april": return APRIL;
case "5":
case "may": return MAY;
case "6":
case "jun":
case "june": return JUNE;
case "7":
case "jul":
case "july": return JULY;
case "8":
case "aug":
case "august": return AUGUST;
case "9":
case "sep":
case "september": return SEPTEMBER;
case "10":
case "oct":
case "october": return OCTOBER;
case "11":
case "nov":
case "november": return NOVEMBER;
case "12":
case "dec":
case "last":
case "december": return DECEMBER;
default:
throw new YearTimes.ParseException("unknown month [" + day + "]");
}
}
@Override
public String toString() {
return cronKey;
}
}

View File

@ -0,0 +1,228 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.scheduler.schedule.support;
import org.elasticsearch.alerts.AlertsException;
import org.elasticsearch.alerts.AlertsSettingsException;
import org.elasticsearch.common.base.Joiner;
import org.elasticsearch.common.collect.ImmutableSet;
import org.elasticsearch.common.primitives.Ints;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
/**
*
*/
public class MonthTimes implements Times {
public static final String LAST = "last_day";
public static final String FIRST = "first_day";
public static final int[] DEFAULT_DAYS = new int[] { 1 };
public static final DayTimes[] DEFAULT_TIMES = new DayTimes[] { new DayTimes() };
private final int[] days;
private final DayTimes[] times;
public MonthTimes() {
this(DEFAULT_DAYS, DEFAULT_TIMES);
}
public MonthTimes(int[] days, DayTimes[] times) {
this.days = days.length == 0 ? DEFAULT_DAYS : days;
Arrays.sort(this.days);
this.times = times.length == 0 ? DEFAULT_TIMES : times;
validate();
}
void validate() {
for (int day : days) {
if (day < 1 || day > 32) { //32 represents the last day of the month
throw new AlertsSettingsException("invalid month day [" + day + "]");
}
}
for (DayTimes dayTimes : times) {
dayTimes.validate();
}
}
public int[] days() {
return days;
}
public DayTimes[] times() {
return times;
}
public Set<String> crons() {
Set<String> crons = new HashSet<>();
for (DayTimes times : this.times) {
String hrsStr = Ints.join(",", times.hour);
String minsStr = Ints.join(",", times.minute);
String daysStr = Ints.join(",", this.days);
daysStr = daysStr.replace("32", "L");
crons.add("0 " + minsStr + " " + hrsStr + " " + daysStr + " * ?");
}
return crons;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MonthTimes that = (MonthTimes) o;
if (!Arrays.equals(days, that.days)) return false;
// order doesn't matter
if (!ImmutableSet.copyOf(times).equals(ImmutableSet.copyOf(that.times))) return false;
return true;
}
@Override
public int hashCode() {
int result = Arrays.hashCode(days);
result = 31 * result + Arrays.hashCode(times);
return result;
}
@Override
public String toString() {
return "days [" + Ints.join(",", days) + "], times [" + Joiner.on(",").join(times) + "]";
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return builder.startObject()
.field(DAY_FIELD.getPreferredName(), days)
.field(TIME_FIELD.getPreferredName(), (Object[]) times)
.endObject();
}
public static Builder builder() {
return new Builder();
}
public static MonthTimes parse(XContentParser parser, XContentParser.Token token) throws IOException, ParseException {
if (token != XContentParser.Token.START_OBJECT) {
throw new ParseException("could not parse month times. expected an object, but found [" + token + "]");
}
Set<Integer> daysSet = new HashSet<>();
Set<DayTimes> timesSet = new HashSet<>();
String currentFieldName = null;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if (DAY_FIELD.match(currentFieldName)) {
if (token.isValue()) {
daysSet.add(parseDayValue(parser, token));
} else if (token == XContentParser.Token.START_ARRAY) {
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
daysSet.add(parseDayValue(parser, token));
}
} else {
throw new ParseException("invalid month day value for [on] field. expected string/number value or an array of string/number values, but found [" + token + "]");
}
} else if (TIME_FIELD.match(currentFieldName)) {
if (token != XContentParser.Token.START_ARRAY) {
try {
timesSet.add(DayTimes.parse(parser, token));
} catch (DayTimes.ParseException pe) {
throw new ParseException("invalid time value for field [at] - [" + token + "]", pe);
}
} else {
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
try {
timesSet.add(DayTimes.parse(parser, token));
} catch (DayTimes.ParseException pe) {
throw new ParseException("invalid time value for field [at] - [" + token + "]", pe);
}
}
}
}
}
int[] days = daysSet.isEmpty() ? DEFAULT_DAYS : Ints.toArray(daysSet);
DayTimes[] times = timesSet.isEmpty() ? new DayTimes[] { new DayTimes(0, 0) } : timesSet.toArray(new DayTimes[timesSet.size()]);
return new MonthTimes(days, times);
}
static int parseDayValue(XContentParser parser, XContentParser.Token token) throws IOException {
if (token == XContentParser.Token.VALUE_STRING) {
String value = parser.text().toLowerCase(Locale.ROOT);
if (LAST.equals(value)) {
return 32;
}
if (FIRST.equals(value)) {
return 1;
}
try {
return Integer.parseInt(value);
} catch (NumberFormatException nfe) {
throw new MonthTimes.ParseException("invalid month day value. string value [" + value + "] cannot be ");
}
}
if (token == XContentParser.Token.VALUE_NUMBER) {
return parser.intValue();
}
throw new MonthTimes.ParseException("invalid month day value. expected a string or a number value, but found [" + token + "]");
}
public static class ParseException extends AlertsException {
public ParseException(String msg) {
super(msg);
}
public ParseException(String msg, Throwable cause) {
super(msg, cause);
}
}
public static class Builder {
private final Set<Integer> days = new HashSet<>();
private final Set<DayTimes> times = new HashSet<>();
private Builder() {
}
public Builder on(int... days) {
this.days.addAll(Ints.asList(days));
return this;
}
public Builder at(int hour, int minute) {
times.add(new DayTimes(hour, minute));
return this;
}
public Builder atRoundHour(int... hours) {
times.add(new DayTimes(hours, new int[] { 0 }));
return this;
}
public Builder atNoon() {
times.add(DayTimes.NOON);
return this;
}
public Builder atMidnight() {
times.add(DayTimes.MIDNIGHT);
return this;
}
public MonthTimes build() {
return new MonthTimes(Ints.toArray(days), times.toArray(new DayTimes[times.size()]));
}
}
}

View File

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.scheduler.schedule.support;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.xcontent.ToXContent;
/**
*
*/
public interface Times extends ToXContent {
public static final ParseField MONTH_FIELD = new ParseField("in", "month");
public static final ParseField DAY_FIELD = new ParseField("on", "day");
public static final ParseField TIME_FIELD = new ParseField("at", "time");
public static final ParseField HOUR_FIELD = new ParseField("hour");
public static final ParseField MINUTE_FIELD = new ParseField("minute");
}

View File

@ -0,0 +1,201 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.scheduler.schedule.support;
import org.elasticsearch.alerts.AlertsException;
import org.elasticsearch.common.collect.ImmutableSet;
import org.elasticsearch.common.primitives.Ints;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import java.io.IOException;
import java.util.*;
/**
*
*/
public class WeekTimes implements Times {
public static final EnumSet<DayOfWeek> DEFAULT_DAYS = EnumSet.of(DayOfWeek.MONDAY);
public static final DayTimes[] DEFAULT_TIMES = new DayTimes[] { new DayTimes() };
private final EnumSet<DayOfWeek> days;
private final DayTimes[] times;
public WeekTimes() {
this(DEFAULT_DAYS, DEFAULT_TIMES);
}
public WeekTimes(DayOfWeek day, DayTimes times) {
this(day, new DayTimes[] { times });
}
public WeekTimes(DayOfWeek day, DayTimes[] times) {
this(EnumSet.of(day), times);
}
public WeekTimes(EnumSet<DayOfWeek> days, DayTimes[] times) {
this.days = days.isEmpty() ? DEFAULT_DAYS : days;
this.times = times.length == 0 ? DEFAULT_TIMES : times;
}
public EnumSet<DayOfWeek> days() {
return days;
}
public DayTimes[] times() {
return times;
}
public Set<String> crons() {
Set<String> crons = new HashSet<>();
for (DayTimes times : this.times) {
String hrsStr = Ints.join(",", times.hour);
String minsStr = Ints.join(",", times.minute);
String daysStr = DayOfWeek.cronPart(this.days);
crons.add("0 " + minsStr + " " + hrsStr + " ? * " + daysStr);
}
return crons;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
WeekTimes that = (WeekTimes) o;
if (!days.equals(that.days)) return false;
// we don't care about order
if (!ImmutableSet.copyOf(times).equals(ImmutableSet.copyOf(that.times))) return false;
return true;
}
@Override
public int hashCode() {
int result = days.hashCode();
result = 31 * result + Arrays.hashCode(times);
return result;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return builder.startObject()
.field(DAY_FIELD.getPreferredName(), days)
.field(TIME_FIELD.getPreferredName(), (Object[]) times)
.endObject();
}
public static Builder builder() {
return new Builder();
}
public static WeekTimes parse(XContentParser parser, XContentParser.Token token) throws IOException, ParseException {
if (token != XContentParser.Token.START_OBJECT) {
throw new ParseException("could not parse week times. expected an object, but found [" + token + "]");
}
Set<DayOfWeek> daysSet = new HashSet<>();
Set<DayTimes> timesSet = new HashSet<>();
String currentFieldName = null;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if (DAY_FIELD.match(currentFieldName)) {
if (token.isValue()) {
daysSet.add(parseDayValue(parser, token));
} else if (token == XContentParser.Token.START_ARRAY) {
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
daysSet.add(parseDayValue(parser, token));
}
} else {
throw new ParseException("invalid week day value for [on] field. expected string/number value or an array of string/number values, but found [" + token + "]");
}
} else if (TIME_FIELD.match(currentFieldName)) {
if (token != XContentParser.Token.START_ARRAY) {
try {
timesSet.add(DayTimes.parse(parser, token));
} catch (DayTimes.ParseException pe) {
throw new ParseException("invalid time value for field [at] - [" + token + "]", pe);
}
} else {
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
try {
timesSet.add(DayTimes.parse(parser, token));
} catch (DayTimes.ParseException pe) {
throw new ParseException("invalid time value for field [at] - [" + token + "]", pe);
}
}
}
}
}
EnumSet<DayOfWeek> days = daysSet.isEmpty() ? EnumSet.of(DayOfWeek.MONDAY) : EnumSet.copyOf(daysSet);
DayTimes[] times = timesSet.isEmpty() ? new DayTimes[] { new DayTimes(0, 0) } : timesSet.toArray(new DayTimes[timesSet.size()]);
return new WeekTimes(days, times);
}
static DayOfWeek parseDayValue(XContentParser parser, XContentParser.Token token) throws IOException {
if (token == XContentParser.Token.VALUE_STRING) {
return DayOfWeek.resolve(parser.text());
}
if (token == XContentParser.Token.VALUE_NUMBER) {
return DayOfWeek.resolve(parser.intValue());
}
throw new WeekTimes.ParseException("invalid weekly day value. expected a string or a number value, but found [" + token + "]");
}
public static class ParseException extends AlertsException {
public ParseException(String msg) {
super(msg);
}
public ParseException(String msg, Throwable cause) {
super(msg, cause);
}
}
public static class Builder {
private final Set<DayOfWeek> days = new HashSet<>();
private final Set<DayTimes> times = new HashSet<>();
private Builder() {
}
public Builder on(DayOfWeek... days) {
Collections.addAll(this.days, days);
return this;
}
public Builder at(int hour, int minute) {
times.add(new DayTimes(hour, minute));
return this;
}
public Builder atRoundHour(int... hours) {
times.add(new DayTimes(hours, new int[] { 0 }));
return this;
}
public Builder atNoon() {
times.add(DayTimes.NOON);
return this;
}
public Builder atMidnight() {
times.add(DayTimes.MIDNIGHT);
return this;
}
public WeekTimes build() {
EnumSet<DayOfWeek> dow = days.isEmpty() ? WeekTimes.DEFAULT_DAYS : EnumSet.copyOf(days);
return new WeekTimes(dow, times.toArray(new DayTimes[times.size()]));
}
}
}

View File

@ -0,0 +1,240 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.scheduler.schedule.support;
import org.elasticsearch.alerts.AlertsException;
import org.elasticsearch.alerts.AlertsSettingsException;
import org.elasticsearch.common.base.Joiner;
import org.elasticsearch.common.collect.ImmutableSet;
import org.elasticsearch.common.primitives.Ints;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import java.io.IOException;
import java.util.*;
/**
*
*/
public class YearTimes implements Times {
public static final EnumSet<Month> DEFAULT_MONTHS = EnumSet.of(Month.JANUARY);
public static final int[] DEFAULT_DAYS = new int[] { 1 };
public static final DayTimes[] DEFAULT_TIMES = new DayTimes[] { new DayTimes() };
private final EnumSet<Month> months;
private final int[] days;
private final DayTimes[] times;
public YearTimes() {
this(DEFAULT_MONTHS, DEFAULT_DAYS, DEFAULT_TIMES);
}
public YearTimes(EnumSet<Month> months, int[] days, DayTimes[] times) {
this.months = months.isEmpty() ? DEFAULT_MONTHS : months;
this.days = days.length == 0 ? DEFAULT_DAYS : days;
Arrays.sort(this.days);
this.times = times.length == 0 ? DEFAULT_TIMES : times;
validate();
}
void validate() {
for (int day : days) {
if (day < 1 || day > 32) { //32 represents the last day of the month
throw new AlertsSettingsException("invalid month day [" + day + "]");
}
}
for (DayTimes dayTimes : times) {
dayTimes.validate();
}
}
public EnumSet<Month> months() {
return months;
}
public int[] days() {
return days;
}
public DayTimes[] times() {
return times;
}
public Set<String> crons() {
Set<String> crons = new HashSet<>();
for (DayTimes times : this.times) {
String hrsStr = Ints.join(",", times.hour);
String minsStr = Ints.join(",", times.minute);
String daysStr = Ints.join(",", this.days);
daysStr = daysStr.replace("32", "L");
String monthsStr = Joiner.on(",").join(months);
crons.add("0 " + minsStr + " " + hrsStr + " " + daysStr + " " + monthsStr + " ?");
}
return crons;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
YearTimes that = (YearTimes) o;
if (!Arrays.equals(days, that.days)) return false;
if (!months.equals(that.months)) return false;
// order doesn't matter
if (!ImmutableSet.copyOf(times).equals(ImmutableSet.copyOf(that.times))) return false;
return true;
}
@Override
public int hashCode() {
int result = months.hashCode();
result = 31 * result + Arrays.hashCode(days);
result = 31 * result + Arrays.hashCode(times);
return result;
}
@Override
public String toString() {
return "months [" + Joiner.on(",").join(months) + "], days [" + Ints.join(",", days) + "], times [" + Joiner.on(",").join(times) + "]";
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return builder.startObject()
.field(MONTH_FIELD.getPreferredName(), months)
.field(DAY_FIELD.getPreferredName(), days)
.field(TIME_FIELD.getPreferredName(), (Object[]) times)
.endObject();
}
public static Builder builder() {
return new Builder();
}
public static YearTimes parse(XContentParser parser, XContentParser.Token token) throws IOException, ParseException {
if (token != XContentParser.Token.START_OBJECT) {
throw new ParseException("could not parse year times. expected an object, but found [" + token + "]");
}
Set<Month> monthsSet = new HashSet<>();
Set<Integer> daysSet = new HashSet<>();
Set<DayTimes> timesSet = new HashSet<>();
String currentFieldName = null;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if (MONTH_FIELD.match(currentFieldName)) {
if (token.isValue()) {
monthsSet.add(parseMonthValue(parser, token));
} else if (token == XContentParser.Token.START_ARRAY) {
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
monthsSet.add(parseMonthValue(parser, token));
}
} else {
throw new ParseException("invalid year month value for [" + currentFieldName + "] field. expected string/number value or an array of string/number values, but found [" + token + "]");
}
} else if (DAY_FIELD.match(currentFieldName)) {
if (token.isValue()) {
daysSet.add(MonthTimes.parseDayValue(parser, token));
} else if (token == XContentParser.Token.START_ARRAY) {
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
daysSet.add(MonthTimes.parseDayValue(parser, token));
}
} else {
throw new ParseException("invalid year day value for [on] field. expected string/number value or an array of string/number values, but found [" + token + "]");
}
} else if (TIME_FIELD.match(currentFieldName)) {
if (token != XContentParser.Token.START_ARRAY) {
try {
timesSet.add(DayTimes.parse(parser, token));
} catch (DayTimes.ParseException pe) {
throw new ParseException("invalid time value for field [at] - [" + token + "]", pe);
}
} else {
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
try {
timesSet.add(DayTimes.parse(parser, token));
} catch (DayTimes.ParseException pe) {
throw new ParseException("invalid time value for field [at] - [" + token + "]", pe);
}
}
}
}
}
EnumSet<Month> months = monthsSet.isEmpty() ? DEFAULT_MONTHS : EnumSet.copyOf(monthsSet);
int[] days = daysSet.isEmpty() ? DEFAULT_DAYS : Ints.toArray(daysSet);
DayTimes[] times = timesSet.isEmpty() ? new DayTimes[] { new DayTimes(0, 0) } : timesSet.toArray(new DayTimes[timesSet.size()]);
return new YearTimes(months, days, times);
}
static Month parseMonthValue(XContentParser parser, XContentParser.Token token) throws IOException {
if (token == XContentParser.Token.VALUE_STRING) {
return Month.resolve(parser.text());
}
if (token == XContentParser.Token.VALUE_NUMBER) {
return Month.resolve(parser.intValue());
}
throw new YearTimes.ParseException("invalid year month value. expected a string or a number value, but found [" + token + "]");
}
public static class ParseException extends AlertsException {
public ParseException(String msg) {
super(msg);
}
public ParseException(String msg, Throwable cause) {
super(msg, cause);
}
}
public static class Builder {
private final Set<Month> months = new HashSet<>();
private final Set<Integer> days = new HashSet<>();
private final Set<DayTimes> times = new HashSet<>();
private Builder() {
}
public Builder in(Month... months) {
Collections.addAll(this.months, months);
return this;
}
public Builder on(int... days) {
this.days.addAll(Ints.asList(days));
return this;
}
public Builder at(int hour, int minute) {
times.add(new DayTimes(hour, minute));
return this;
}
public Builder atRoundHour(int... hours) {
times.add(new DayTimes(hours, new int[] { 0 }));
return this;
}
public Builder atNoon() {
times.add(DayTimes.NOON);
return this;
}
public Builder atMidnight() {
times.add(DayTimes.MIDNIGHT);
return this;
}
public YearTimes build() {
return new YearTimes(EnumSet.copyOf(months), Ints.toArray(days), times.toArray(new DayTimes[times.size()]));
}
}
}

View File

@ -0,0 +1,283 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.scheduler;
import org.apache.lucene.util.LuceneTestCase.Slow;
import org.elasticsearch.alerts.AlertsPlugin;
import org.elasticsearch.alerts.scheduler.schedule.Schedule;
import org.elasticsearch.alerts.scheduler.schedule.support.DayOfWeek;
import org.elasticsearch.alerts.scheduler.schedule.support.WeekTimes;
import org.elasticsearch.common.joda.time.DateTime;
import org.elasticsearch.common.joda.time.DateTimeZone;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.elasticsearch.threadpool.ThreadPool;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.elasticsearch.alerts.scheduler.schedule.Schedules.*;
import static org.hamcrest.Matchers.is;
/**
*
*/
@Slow
public class InternalSchedulerTests extends ElasticsearchTestCase {
private ThreadPool threadPool;
private InternalScheduler scheduler;
@Before
public void init() throws Exception {
AlertsPlugin plugin = new AlertsPlugin(ImmutableSettings.EMPTY);
Settings settings = ImmutableSettings.builder()
.put(plugin.additionalSettings())
.put("name", "test")
.build();
threadPool = new ThreadPool(settings, null);
scheduler = new InternalScheduler(ImmutableSettings.EMPTY, threadPool);
}
@After
public void cleanup() throws Exception {
scheduler.stop();
threadPool.shutdownNow();
}
@Test
public void testStart() throws Exception {
int count = randomIntBetween(2, 5);
final CountDownLatch latch = new CountDownLatch(count);
List<Scheduler.Job> jobs = new ArrayList<>();
for (int i = 0; i < count; i++) {
jobs.add(new SimpleJob(String.valueOf(i), interval("3s")));
}
final BitSet bits = new BitSet(count);
scheduler.addListener(new Scheduler.Listener() {
@Override
public void fire(String jobName, DateTime scheduledFireTime, DateTime fireTime) {
int index = Integer.parseInt(jobName);
if (!bits.get(index)) {
logger.info("job [" + index + "] first fire: " + new DateTime());
bits.set(index);
} else {
latch.countDown();
logger.info("job [" + index + "] second fire: " + new DateTime());
}
}
});
scheduler.start(jobs);
if (!latch.await(5, TimeUnit.SECONDS)) {
fail("waiting too long for all alerts to be fired");
}
scheduler.stop();
assertThat(bits.cardinality(), is(count));
}
@Test
public void testAdd_Hourly() throws Exception {
final String name = "job_name";
final CountDownLatch latch = new CountDownLatch(1);
scheduler.start(Collections.<Scheduler.Job>emptySet());
scheduler.addListener(new Scheduler.Listener() {
@Override
public void fire(String jobName, DateTime scheduledFireTime, DateTime fireTime) {
assertThat(jobName, is(name));
logger.info("triggered job on [{}]", new DateTime());
latch.countDown();
}
});
DateTime now = new DateTime(DateTimeZone.UTC);
Minute minOfHour = new Minute(now);
if (now.getSecondOfMinute() < 58) {
minOfHour.inc(1);
} else {
minOfHour.inc(2);
}
int minute = minOfHour.value;
logger.info("scheduling hourly job [{}]", minute);
logger.info("current date [{}]", now);
scheduler.add(new SimpleJob(name, hourly(minute)));
long secondsToWait = now.getSecondOfMinute() < 29 ? 62 - now.getSecondOfMinute() : 122 - now.getSecondOfMinute();
logger.info("waiting at least [{}] seconds for response", secondsToWait);
if (!latch.await(secondsToWait, TimeUnit.SECONDS)) {
fail("waiting too long for alert to be fired");
}
}
@Test
public void testAdd_Daily() throws Exception {
final String name = "job_name";
final CountDownLatch latch = new CountDownLatch(1);
scheduler.start(Collections.<Scheduler.Job>emptySet());
scheduler.addListener(new Scheduler.Listener() {
@Override
public void fire(String jobName, DateTime scheduledFireTime, DateTime fireTime) {
assertThat(jobName, is(name));
logger.info("triggered job on [{}]", new DateTime());
latch.countDown();
}
});
DateTime now = new DateTime(DateTimeZone.UTC);
Minute minOfHour = new Minute(now);
Hour hourOfDay = new Hour(now);
boolean jumpedHour = now.getSecondOfMinute() < 29 ? minOfHour.inc(1) : minOfHour.inc(2);
int minute = minOfHour.value;
if (jumpedHour) {
hourOfDay.inc(1);
}
int hour = hourOfDay.value;
logger.info("scheduling hourly job [{}:{}]", hour, minute);
logger.info("current date [{}]", now);
scheduler.add(new SimpleJob(name, daily().at(hour, minute).build()));
// 30 sec is the default idle time of quartz
long secondsToWait = now.getSecondOfMinute() < 29 ? 62 - now.getSecondOfMinute() : 122 - now.getSecondOfMinute();
logger.info("waiting at least [{}] seconds for response", secondsToWait);
if (!latch.await(secondsToWait, TimeUnit.SECONDS)) {
fail("waiting too long for alert to be fired");
}
}
@Test
public void testAdd_Weekly() throws Exception {
final String name = "job_name";
final CountDownLatch latch = new CountDownLatch(1);
scheduler.start(Collections.<Scheduler.Job>emptySet());
scheduler.addListener(new Scheduler.Listener() {
@Override
public void fire(String jobName, DateTime scheduledFireTime, DateTime fireTime) {
assertThat(jobName, is(name));
logger.info("triggered job on [{}]", new DateTime());
latch.countDown();
}
});
DateTime now = new DateTime(DateTimeZone.UTC);
Minute minOfHour = new Minute(now);
Hour hourOfDay = new Hour(now);
Day dayOfWeek = new Day(now);
boolean jumpedHour = now.getSecondOfMinute() < 29 ? minOfHour.inc(1) : minOfHour.inc(2);
int minute = minOfHour.value;
if (jumpedHour && hourOfDay.inc(1)) {
dayOfWeek.inc(1);
}
int hour = hourOfDay.value;
DayOfWeek day = dayOfWeek.day();
logger.info("scheduling hourly job [{} {}:{}]", day, hour, minute);
logger.info("current date [{}]", now);
scheduler.add(new SimpleJob(name, weekly().time(WeekTimes.builder().on(day).at(hour, minute).build()).build()));
// 30 sec is the default idle time of quartz
long secondsToWait = now.getSecondOfMinute() < 29 ? 62 - now.getSecondOfMinute() : 122 - now.getSecondOfMinute();
logger.info("waiting at least [{}] seconds for response", secondsToWait);
if (!latch.await(secondsToWait, TimeUnit.SECONDS)) {
fail("waiting too long for alert to be fired");
}
}
static class SimpleJob implements Scheduler.Job {
private final String name;
private final Schedule schedule;
public SimpleJob(String name, Schedule schedule) {
this.name = name;
this.schedule = schedule;
}
@Override
public String name() {
return name;
}
@Override
public Schedule schedule() {
return schedule;
}
}
static class Hour {
int value;
Hour(DateTime time) {
value = time.getHourOfDay();
}
/**
* increments the hour and returns whether the day jumped. (note, only supports increment steps < 24)
*/
boolean inc(int inc) {
value += inc;
if (value > 23) {
value %= 24;
return true;
}
return false;
}
}
static class Minute {
int value;
Minute(DateTime time) {
value = time.getMinuteOfHour();
}
/**
* increments the minute and returns whether the hour jumped. (note, only supports increment steps < 60)
*/
boolean inc(int inc) {
value += inc;
if (value > 59) {
value %= 60;
return true;
}
return false;
}
}
static class Day {
int value;
Day(DateTime time) {
value = time.getDayOfWeek() - 1;
}
/**
* increments the minute and returns whether the week jumped. (note, only supports increment steps < 8)
*/
boolean inc(int inc) {
value += inc;
if (value > 6) {
value %= 7;
return true;
}
return false;
}
DayOfWeek day() {
switch (value) {
case 0 : return DayOfWeek.MONDAY;
case 1 : return DayOfWeek.TUESDAY;
case 2 : return DayOfWeek.WEDNESDAY;
case 3 : return DayOfWeek.THURSDAY;
case 4 : return DayOfWeek.FRIDAY;
case 5 : return DayOfWeek.SATURDAY;
default : return DayOfWeek.SUNDAY;
}
}
}
}

View File

@ -0,0 +1,98 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.scheduler.schedule;
import org.elasticsearch.alerts.AlertsSettingsException;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.junit.Test;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.hamcrest.Matchers.*;
/**
*
*/
public class CronScheduleTests extends ScheduleTestCase {
@Test(expected = CronSchedule.ValidationException.class)
public void testInvalid() throws Exception {
new CronSchedule("0 * * *");
fail("expecting a validation error to be thrown when creating a cron schedule with invalid cron expression");
}
@Test
public void testParse_Single() throws Exception {
XContentBuilder builder = jsonBuilder().value("0 0/5 * * * ?");
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken();
CronSchedule schedule = new CronSchedule.Parser().parse(parser);
assertThat(schedule.crons(), arrayWithSize(1));
assertThat(schedule.crons()[0], is("0 0/5 * * * ?"));
}
@Test
public void testParse_Multiple() throws Exception {
XContentBuilder builder = jsonBuilder().value(new String[] {
"0 0/1 * * * ?",
"0 0/2 * * * ?",
"0 0/3 * * * ?"
});
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken();
CronSchedule schedule = new CronSchedule.Parser().parse(parser);
assertThat(schedule.crons(), arrayWithSize(3));
assertThat(schedule.crons(), hasItemInArray("0 0/1 * * * ?"));
assertThat(schedule.crons(), hasItemInArray("0 0/2 * * * ?"));
assertThat(schedule.crons(), hasItemInArray("0 0/3 * * * ?"));
}
@Test
public void testParse_Invalid_BadExpression() throws Exception {
XContentBuilder builder = jsonBuilder().value("0 0/5 * * ?");
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken();
try {
new CronSchedule.Parser().parse(parser);
fail("expected cron parsing to fail when using invalid cron expression");
} catch (AlertsSettingsException ase) {
// expected
assertThat(ase.getCause(), instanceOf(CronSchedule.ValidationException.class));
}
}
@Test(expected = AlertsSettingsException.class)
public void testParse_Invalid_Empty() throws Exception {
XContentBuilder builder = jsonBuilder();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken();
new CronSchedule.Parser().parse(parser);
}
@Test(expected = AlertsSettingsException.class)
public void testParse_Invalid_Object() throws Exception {
XContentBuilder builder = jsonBuilder().startObject().endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken();
new CronSchedule.Parser().parse(parser);
}
@Test(expected = AlertsSettingsException.class)
public void testParse_Invalid_EmptyArray() throws Exception {
XContentBuilder builder = jsonBuilder().value(new String[0]);
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken();
new CronSchedule.Parser().parse(parser);
}
}

View File

@ -0,0 +1,204 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.scheduler.schedule;
import com.carrotsearch.randomizedtesting.annotations.Repeat;
import org.elasticsearch.alerts.AlertsSettingsException;
import org.elasticsearch.alerts.scheduler.schedule.support.DayTimes;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.primitives.Ints;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.junit.Test;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.hamcrest.Matchers.*;
/**
*
*/
public class DailyScheduleTests extends ScheduleTestCase {
@Test
public void test_Default() throws Exception {
DailySchedule schedule = new DailySchedule();
String[] crons = schedule.crons();
assertThat(crons, arrayWithSize(1));
assertThat(crons, arrayContaining("0 0 0 * * ?"));
}
@Test @Repeat(iterations = 20)
public void test_SingleTime() throws Exception {
DayTimes time = validDayTime();
DailySchedule schedule = new DailySchedule(time);
String[] crons = schedule.crons();
assertThat(crons, arrayWithSize(1));
assertThat(crons, arrayContaining("0 " + Ints.join(",", time.minute()) + " " + Ints.join(",", time.hour()) + " * * ?"));
}
@Test @Repeat(iterations = 20)
public void test_SingleTime_Invalid() throws Exception {
try {
HourAndMinute ham = invalidDayTime();
new DayTimes(ham.hour, ham.minute);
fail("expected either a parse exception or an alerts settings exception on invalid time input");
} catch (DayTimes.ParseException pe) {
// expected
} catch (AlertsSettingsException ase) {
// expected
}
}
@Test @Repeat(iterations = 20)
public void test_MultipleTimes() throws Exception {
DayTimes[] times = validDayTimes();
DailySchedule schedule = new DailySchedule(times);
String[] crons = schedule.crons();
assertThat(crons, arrayWithSize(times.length));
for (DayTimes time : times) {
assertThat(crons, hasItemInArray("0 " + Ints.join(",", time.minute()) + " " + Ints.join(",", time.hour()) + " * * ?"));
}
}
@Test
public void testParser_Empty() throws Exception {
XContentBuilder builder = jsonBuilder().startObject().endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
DailySchedule schedule = new DailySchedule.Parser().parse(parser);
assertThat(schedule, notNullValue());
assertThat(schedule.times().length, is(1));
assertThat(schedule.times()[0], is(new DayTimes(0, 0)));
}
@Test @Repeat(iterations = 20)
public void testParser_SingleTime_Object() throws Exception {
DayTimes time = validDayTime();
XContentBuilder builder = jsonBuilder()
.startObject()
.startObject("at")
.field("hour", time.hour())
.field("minute", time.minute())
.endObject()
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
DailySchedule schedule = new DailySchedule.Parser().parse(parser);
assertThat(schedule, notNullValue());
assertThat(schedule.times().length, is(1));
assertThat(schedule.times()[0], is(time));
}
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
public void testParser_SingleTime_Object_Invalid() throws Exception {
HourAndMinute time = invalidDayTime();
XContentBuilder builder = jsonBuilder()
.startObject()
.startObject("at")
.field("hour", time.hour)
.field("minute", time.minute)
.endObject()
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
new DailySchedule.Parser().parse(parser);
}
@Test @Repeat(iterations = 20)
public void testParser_SingleTime_String() throws Exception {
String timeStr = validDayTimeStr();
XContentBuilder builder = jsonBuilder()
.startObject()
.field("at", timeStr)
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
DailySchedule schedule = new DailySchedule.Parser().parse(parser);
assertThat(schedule, notNullValue());
assertThat(schedule.times().length, is(1));
assertThat(schedule.times()[0], is(DayTimes.parse(timeStr)));
}
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
public void testParser_SingleTime_String_Invalid() throws Exception {
XContentBuilder builder = jsonBuilder()
.startObject()
.field("at", invalidDayTimeStr())
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
new DailySchedule.Parser().parse(parser);
}
@Test @Repeat(iterations = 20)
public void testParser_MultipleTimes_Objects() throws Exception {
DayTimes[] times = validDayTimesFromNumbers();
XContentBuilder builder = jsonBuilder()
.startObject()
.array("at", (Object[]) times)
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
DailySchedule schedule = new DailySchedule.Parser().parse(parser);
assertThat(schedule, notNullValue());
assertThat(schedule.times().length, is(times.length));
for (int i = 0; i < times.length; i++) {
assertThat(schedule.times(), hasItemInArray(times[i]));
}
}
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
public void testParser_MultipleTimes_Objects_Invalid() throws Exception {
HourAndMinute[] times = invalidDayTimes();
XContentBuilder builder = jsonBuilder()
.startObject()
.array("at", (Object[]) times)
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
new DailySchedule.Parser().parse(parser);
}
@Test @Repeat(iterations = 20)
public void testParser_MultipleTimes_Strings() throws Exception {
DayTimes[] times = validDayTimesFromStrings();
XContentBuilder builder = jsonBuilder()
.startObject()
.array("at", (Object[]) times)
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
DailySchedule schedule = new DailySchedule.Parser().parse(parser);
assertThat(schedule, notNullValue());
assertThat(schedule.times().length, is(times.length));
for (int i = 0; i < times.length; i++) {
assertThat(schedule.times(), hasItemInArray(times[i]));
}
}
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
public void testParser_MultipleTimes_Strings_Invalid() throws Exception {
String[] times = invalidDayTimesAsStrings();
XContentBuilder builder = jsonBuilder()
.startObject()
.field("at", times)
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
new DailySchedule.Parser().parse(parser);
}
}

View File

@ -0,0 +1,194 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.scheduler.schedule;
import com.carrotsearch.randomizedtesting.annotations.Repeat;
import org.elasticsearch.alerts.AlertsSettingsException;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.collect.Collections2;
import org.elasticsearch.common.primitives.Ints;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.junit.Test;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.hamcrest.Matchers.*;
/**
*
*/
public class HourlyScheduleTests extends ScheduleTestCase {
@Test
public void test_Default() throws Exception {
HourlySchedule schedule = new HourlySchedule();
String[] crons = schedule.crons();
assertThat(crons, arrayWithSize(1));
assertThat(crons, arrayContaining("0 0 * * * ?"));
}
@Test @Repeat(iterations = 20)
public void test_SingleMinute() throws Exception {
int minute = validMinute();
HourlySchedule schedule = new HourlySchedule(minute);
String[] crons = schedule.crons();
assertThat(crons, arrayWithSize(1));
assertThat(crons, arrayContaining("0 " + minute + " * * * ?"));
}
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
public void test_SingleMinute_Invalid() throws Exception {
new HourlySchedule(invalidMinute());
}
@Test @Repeat(iterations = 20)
public void test_MultipleMinutes() throws Exception {
int[] minutes = validMinutes();
String minutesStr = Ints.join(",", minutes);
HourlySchedule schedule = new HourlySchedule(minutes);
String[] crons = schedule.crons();
assertThat(crons, arrayWithSize(1));
assertThat(crons, arrayContaining("0 " + minutesStr + " * * * ?"));
}
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
public void test_MultipleMinutes_Invalid() throws Exception {
int[] minutes = invalidMinutes();
new HourlySchedule(minutes);
}
@Test
public void testParser_Empty() throws Exception {
XContentBuilder builder = jsonBuilder().startObject().endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
HourlySchedule schedule = new HourlySchedule.Parser().parse(parser);
assertThat(schedule, notNullValue());
assertThat(schedule.minutes().length, is(1));
assertThat(schedule.minutes()[0], is(0));
}
@Test @Repeat(iterations = 20)
public void testParser_SingleMinute_Number() throws Exception {
int minute = validMinute();
XContentBuilder builder = jsonBuilder()
.startObject()
.field("minute", minute)
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
HourlySchedule schedule = new HourlySchedule.Parser().parse(parser);
assertThat(schedule, notNullValue());
assertThat(schedule.minutes().length, is(1));
assertThat(schedule.minutes()[0], is(minute));
}
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
public void testParser_SingleMinute_Number_Invalid() throws Exception {
XContentBuilder builder = jsonBuilder()
.startObject()
.field("minute", invalidMinute())
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
new HourlySchedule.Parser().parse(parser);
}
@Test @Repeat(iterations = 20)
public void testParser_SingleMinute_String() throws Exception {
int minute = validMinute();
XContentBuilder builder = jsonBuilder()
.startObject()
.field("minute", String.valueOf(minute))
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
HourlySchedule schedule = new HourlySchedule.Parser().parse(parser);
assertThat(schedule, notNullValue());
assertThat(schedule.minutes().length, is(1));
assertThat(schedule.minutes()[0], is(minute));
}
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
public void testParser_SingleMinute_String_Invalid() throws Exception {
XContentBuilder builder = jsonBuilder()
.startObject()
.field("minute", String.valueOf(invalidMinute()))
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
new HourlySchedule.Parser().parse(parser);
}
@Test @Repeat(iterations = 20)
public void testParser_MultipleMinutes_Numbers() throws Exception {
int[] minutes = validMinutes();
XContentBuilder builder = jsonBuilder()
.startObject()
.field("minute", Ints.asList(minutes))
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
HourlySchedule schedule = new HourlySchedule.Parser().parse(parser);
assertThat(schedule, notNullValue());
assertThat(schedule.minutes().length, is(minutes.length));
for (int i = 0; i < minutes.length; i++) {
assertThat(Ints.contains(schedule.minutes(), minutes[i]), is(true));
}
}
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
public void testParser_MultipleMinutes_Numbers_Invalid() throws Exception {
int[] minutes = invalidMinutes();
XContentBuilder builder = jsonBuilder()
.startObject()
.field("minute", Ints.asList(minutes))
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
new HourlySchedule.Parser().parse(parser);
}
@Test @Repeat(iterations = 20)
public void testParser_MultipleMinutes_Strings() throws Exception {
int[] minutes = validMinutes();
XContentBuilder builder = jsonBuilder()
.startObject()
.field("minute", Collections2.transform(Ints.asList(minutes), Ints.stringConverter().reverse()))
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
HourlySchedule schedule = new HourlySchedule.Parser().parse(parser);
assertThat(schedule, notNullValue());
assertThat(schedule.minutes().length, is(minutes.length));
for (int i = 0; i < minutes.length; i++) {
assertThat(Ints.contains(schedule.minutes(), minutes[i]), is(true));
}
}
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
public void testParser_MultipleMinutes_Strings_Invalid() throws Exception {
int[] minutes = invalidMinutes();
XContentBuilder builder = jsonBuilder()
.startObject()
.field("minute", Collections2.transform(Ints.asList(minutes), Ints.stringConverter().reverse()))
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
new HourlySchedule.Parser().parse(parser);
}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.scheduler.schedule;
import org.elasticsearch.alerts.AlertsSettingsException;
import org.elasticsearch.alerts.scheduler.schedule.IntervalSchedule.Interval.Unit;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.junit.Test;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
/**
*
*/
public class IntervalScheduleTests extends ElasticsearchTestCase {
@Test
public void testParse_Number() throws Exception {
long value = (long) randomInt();
XContentBuilder builder = jsonBuilder().value(value);
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
IntervalSchedule schedule = new IntervalSchedule.Parser().parse(parser);
assertThat(schedule, notNullValue());
assertThat(schedule.interval().seconds(), is(value));
}
@Test
public void testParse_String() throws Exception {
IntervalSchedule.Interval value = randomTimeValue();
XContentBuilder builder = jsonBuilder().value(value);
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
IntervalSchedule schedule = new IntervalSchedule.Parser().parse(parser);
assertThat(schedule, notNullValue());
assertThat(schedule.interval(), is(value));
}
@Test(expected = AlertsSettingsException.class)
public void testParse_Invalid_String() throws Exception {
XContentBuilder builder = jsonBuilder().value("43S");
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
new IntervalSchedule.Parser().parse(parser);
}
@Test(expected = AlertsSettingsException.class)
public void testParse_Invalid_Object() throws Exception {
XContentBuilder builder = jsonBuilder().startObject().endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
new IntervalSchedule.Parser().parse(parser);
}
private static IntervalSchedule.Interval randomTimeValue() {
Unit unit = Unit.values()[randomIntBetween(0, Unit.values().length - 1)];
return new IntervalSchedule.Interval(randomIntBetween(1, 100), unit);
}
}

View File

@ -0,0 +1,152 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.scheduler.schedule;
import com.carrotsearch.randomizedtesting.annotations.Repeat;
import org.elasticsearch.alerts.AlertsSettingsException;
import org.elasticsearch.alerts.scheduler.schedule.support.DayTimes;
import org.elasticsearch.alerts.scheduler.schedule.support.MonthTimes;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.primitives.Ints;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.junit.Test;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.hamcrest.Matchers.*;
/**
*
*/
public class MonthlyScheduleTests extends ScheduleTestCase {
@Test
public void test_Default() throws Exception {
MonthlySchedule schedule = new MonthlySchedule();
String[] crons = schedule.crons();
assertThat(crons, arrayWithSize(1));
assertThat(crons, arrayContaining("0 0 0 1 * ?"));
}
@Test @Repeat(iterations = 20)
public void test_SingleTime() throws Exception {
MonthTimes time = validMonthTime();
MonthlySchedule schedule = new MonthlySchedule(time);
String[] crons = schedule.crons();
assertThat(crons, arrayWithSize(time.times().length));
for (DayTimes dayTimes : time.times()) {
String minStr = Ints.join(",", dayTimes.minute());
String hrStr = Ints.join(",", dayTimes.hour());
String dayStr = Ints.join(",", time.days());
dayStr = dayStr.replace("32", "L");
assertThat(crons, hasItemInArray("0 " + minStr + " " + hrStr + " " + dayStr + " * ?"));
}
}
@Test @Repeat(iterations = 20)
public void test_MultipleTimes() throws Exception {
MonthTimes[] times = validMonthTimes();
MonthlySchedule schedule = new MonthlySchedule(times);
String[] crons = schedule.crons();
int count = 0;
for (int i = 0; i < times.length; i++) {
count += times[i].times().length;
}
assertThat(crons, arrayWithSize(count));
for (MonthTimes monthTimes : times) {
for (DayTimes dayTimes : monthTimes.times()) {
String minStr = Ints.join(",", dayTimes.minute());
String hrStr = Ints.join(",", dayTimes.hour());
String dayStr = Ints.join(",", monthTimes.days());
dayStr = dayStr.replace("32", "L");
assertThat(crons, hasItemInArray("0 " + minStr + " " + hrStr + " " + dayStr + " * ?"));
}
}
}
@Test @Repeat(iterations = 20)
public void testParser_Empty() throws Exception {
XContentBuilder builder = jsonBuilder().startObject().endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
MonthlySchedule schedule = new MonthlySchedule.Parser().parse(parser);
assertThat(schedule, notNullValue());
assertThat(schedule.times().length, is(1));
assertThat(schedule.times()[0], is(new MonthTimes()));
}
@Test @Repeat(iterations = 20)
public void testParser_SingleTime() throws Exception {
DayTimes time = validDayTime();
Object day = randomDayOfMonth();
XContentBuilder builder = jsonBuilder()
.startObject()
.field("on", day)
.startObject("at")
.field("hour", time.hour())
.field("minute", time.minute())
.endObject()
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
MonthlySchedule schedule = new MonthlySchedule.Parser().parse(parser);
assertThat(schedule, notNullValue());
assertThat(schedule.times().length, is(1));
assertThat(schedule.times()[0].days().length, is(1));
assertThat(schedule.times()[0].days()[0], is(dayOfMonthToInt(day)));
assertThat(schedule.times()[0].times(), arrayWithSize(1));
assertThat(schedule.times()[0].times(), hasItemInArray(time));
}
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
public void testParser_SingleTime_Invalid() throws Exception {
HourAndMinute time = invalidDayTime();
XContentBuilder builder = jsonBuilder()
.startObject()
.field("on", randomBoolean() ? invalidDayOfMonth() : randomDayOfMonth())
.startObject("at")
.field("hour", time.hour)
.field("minute", time.minute)
.endObject()
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
new MonthlySchedule.Parser().parse(parser);
}
@Test @Repeat(iterations = 20)
public void testParser_MultipleTimes() throws Exception {
MonthTimes[] times = validMonthTimes();
XContentBuilder builder = jsonBuilder().value(times);
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
MonthlySchedule schedule = new MonthlySchedule.Parser().parse(parser);
assertThat(schedule, notNullValue());
assertThat(schedule.times().length, is(times.length));
for (MonthTimes time : times) {
assertThat(schedule.times(), hasItemInArray(time));
}
}
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
public void testParser_MultipleTimes_Invalid() throws Exception {
HourAndMinute[] times = invalidDayTimes();
XContentBuilder builder = jsonBuilder()
.startObject()
.field("on", randomDayOfMonth())
.array("at", (Object[]) times)
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
new MonthlySchedule.Parser().parse(parser);
}
}

View File

@ -0,0 +1,142 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.scheduler.schedule;
import com.carrotsearch.randomizedtesting.annotations.Repeat;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.junit.Before;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
import static org.elasticsearch.alerts.scheduler.schedule.Schedules.cron;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.hamcrest.Matchers.*;
/**
*
*/
public class ScheduleRegistryTests extends ScheduleTestCase {
private ScheduleRegistry registry;
@Before
public void init() throws Exception {
Map<String, Schedule.Parser> parsers = new HashMap<>();
parsers.put(IntervalSchedule.TYPE, new IntervalSchedule.Parser());
parsers.put(CronSchedule.TYPE, new CronSchedule.Parser());
parsers.put(HourlySchedule.TYPE, new HourlySchedule.Parser());
parsers.put(DailySchedule.TYPE, new DailySchedule.Parser());
parsers.put(WeeklySchedule.TYPE, new WeeklySchedule.Parser());
parsers.put(MonthlySchedule.TYPE, new MonthlySchedule.Parser());
registry = new ScheduleRegistry(parsers);
}
@Test @Repeat(iterations = 20)
public void testParser_Interval() throws Exception {
IntervalSchedule interval = randomIntervalSchedule();
XContentBuilder builder = jsonBuilder()
.startObject()
.field(IntervalSchedule.TYPE, interval)
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken();
Schedule schedule = registry.parse(parser);
assertThat(schedule, notNullValue());
assertThat(schedule, instanceOf(IntervalSchedule.class));
assertThat((IntervalSchedule) schedule, is(interval));
}
@Test @Repeat(iterations = 20)
public void testParse_Cron() throws Exception {
Object cron = randomBoolean() ?
cron("* 0/5 * * * ?") :
cron("* 0/2 * * * ?", "* 0/3 * * * ?", "* 0/5 * * * ?");
XContentBuilder builder = jsonBuilder()
.startObject()
.field(CronSchedule.TYPE, cron)
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken();
Schedule schedule = registry.parse(parser);
assertThat(schedule, notNullValue());
assertThat(schedule, instanceOf(CronSchedule.class));
assertThat(schedule, is(cron));
}
@Test @Repeat(iterations = 20)
public void testParse_Hourly() throws Exception {
HourlySchedule hourly = randomHourlySchedule();
XContentBuilder builder = jsonBuilder()
.startObject()
.field(HourlySchedule.TYPE, hourly)
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken();
Schedule schedule = registry.parse(parser);
assertThat(schedule, notNullValue());
assertThat(schedule, instanceOf(HourlySchedule.class));
assertThat((HourlySchedule) schedule, equalTo(hourly));
}
@Test @Repeat(iterations = 20)
public void testParse_Daily() throws Exception {
DailySchedule daily = randomDailySchedule();
XContentBuilder builder = jsonBuilder()
.startObject()
.field(DailySchedule.TYPE, daily)
.endObject();
BytesReference bytes = builder.bytes();
System.out.println(bytes.toUtf8());
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken();
Schedule schedule = registry.parse(parser);
assertThat(schedule, notNullValue());
assertThat(schedule, instanceOf(DailySchedule.class));
assertThat((DailySchedule) schedule, equalTo(daily));
}
@Test @Repeat(iterations = 20)
public void testParse_Weekly() throws Exception {
WeeklySchedule weekly = randomWeeklySchedule();
XContentBuilder builder = jsonBuilder()
.startObject()
.field(WeeklySchedule.TYPE, weekly)
.endObject();
BytesReference bytes = builder.bytes();
System.out.println(bytes.toUtf8());
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken();
Schedule schedule = registry.parse(parser);
assertThat(schedule, notNullValue());
assertThat(schedule, instanceOf(WeeklySchedule.class));
assertThat((WeeklySchedule) schedule, equalTo(weekly));
}
@Test @Repeat(iterations = 20)
public void testParse_Monthly() throws Exception {
MonthlySchedule monthly = randomMonthlySchedule();
XContentBuilder builder = jsonBuilder()
.startObject()
.field(MonthlySchedule.TYPE, monthly)
.endObject();
BytesReference bytes = builder.bytes();
System.out.println(bytes.toUtf8());
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken();
Schedule schedule = registry.parse(parser);
assertThat(schedule, notNullValue());
assertThat(schedule, instanceOf(MonthlySchedule.class));
assertThat((MonthlySchedule) schedule, equalTo(monthly));
}
}

View File

@ -0,0 +1,370 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.scheduler.schedule;
import com.google.common.primitives.Ints;
import org.elasticsearch.alerts.scheduler.schedule.IntervalSchedule.Interval.Unit;
import org.elasticsearch.alerts.scheduler.schedule.support.*;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.test.ElasticsearchTestCase;
import java.io.IOException;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Set;
import static org.elasticsearch.alerts.scheduler.schedule.Schedules.*;
/**
*
*/
public abstract class ScheduleTestCase extends ElasticsearchTestCase {
protected static MonthlySchedule randomMonthlySchedule() {
switch (randomIntBetween(1, 4)) {
case 1: return monthly().build();
case 2: return monthly().time(MonthTimes.builder().atMidnight()).build();
case 3: return monthly().time(MonthTimes.builder().on(randomIntBetween(1, 31)).atMidnight()).build();
default: return new MonthlySchedule(validMonthTimes());
}
}
protected static WeeklySchedule randomWeeklySchedule() {
switch (randomIntBetween(1, 4)) {
case 1: return weekly().build();
case 2: return weekly().time(WeekTimes.builder().atMidnight()).build();
case 3: return weekly().time(WeekTimes.builder().on(DayOfWeek.THURSDAY).atMidnight()).build();
default: return new WeeklySchedule(validWeekTimes());
}
}
protected static DailySchedule randomDailySchedule() {
switch (randomIntBetween(1, 4)) {
case 1: return daily().build();
case 2: return daily().atMidnight().build();
case 3: return daily().atNoon().build();
default: return new DailySchedule(validDayTimes());
}
}
protected static HourlySchedule randomHourlySchedule() {
switch (randomIntBetween(1, 4)) {
case 1: return hourly().build();
case 2: return hourly().minutes(randomIntBetween(0, 59)).build();
case 3: return hourly(randomIntBetween(0, 59));
default: return hourly().minutes(validMinutes()).build();
}
}
protected static IntervalSchedule randomIntervalSchedule() {
switch (randomIntBetween(1, 3)) {
case 1: return interval(randomInterval().toString());
case 2: return interval(randomIntBetween(1, 100), randomIntervalUnit());
default: return new IntervalSchedule(randomInterval());
}
}
protected static IntervalSchedule.Interval randomInterval() {
return new IntervalSchedule.Interval(randomIntBetween(1, 100), randomIntervalUnit());
}
protected static Unit randomIntervalUnit() {
return Unit.values()[randomIntBetween(0, Unit.values().length - 1)];
}
protected static YearTimes validYearTime() {
return new YearTimes(randomMonths(), randomDaysOfMonth(), validDayTimes());
}
protected static YearTimes[] validYearTimes() {
int count = randomIntBetween(2, 5);
Set<YearTimes> times = new HashSet<>();
for (int i = 0; i < count; i++) {
times.add(validYearTime());
}
return times.toArray(new YearTimes[times.size()]);
}
protected static MonthTimes validMonthTime() {
return new MonthTimes(randomDaysOfMonth(), validDayTimes());
}
protected static MonthTimes[] validMonthTimes() {
int count = randomIntBetween(2, 5);
Set<MonthTimes> times = new HashSet<>();
for (int i = 0; i < count; i++) {
times.add(validMonthTime());
}
return times.toArray(new MonthTimes[times.size()]);
}
protected static WeekTimes validWeekTime() {
return new WeekTimes(randomDaysOfWeek(), validDayTimes());
}
protected static WeekTimes[] validWeekTimes() {
int count = randomIntBetween(2, 5);
Set<WeekTimes> times = new HashSet<>();
for (int i = 0; i < count; i++) {
times.add(validWeekTime());
}
return times.toArray(new WeekTimes[times.size()]);
}
protected static EnumSet<DayOfWeek> randomDaysOfWeek() {
int count = randomIntBetween(1, DayOfWeek.values().length-1);
Set<DayOfWeek> days = new HashSet<>();
for (int i = 0; i < count; i++) {
days.add(DayOfWeek.values()[randomIntBetween(0, count)]);
}
return EnumSet.copyOf(days);
}
protected static EnumSet<Month> randomMonths() {
int count = randomIntBetween(1, 11);
Set<Month> months = new HashSet<>();
for (int i = 0; i < count; i++) {
months.add(Month.values()[randomIntBetween(0, 11)]);
}
return EnumSet.copyOf(months);
}
protected static Object randomMonth() {
int m = randomIntBetween(1, 14);
switch (m) {
case 13:
return "first";
case 14:
return "last";
default:
return Month.resolve(m);
}
}
protected static int[] randomDaysOfMonth() {
int count = randomIntBetween(1, 5);
Set<Integer> days = new HashSet<>();
for (int i = 0; i < count; i++) {
days.add(randomIntBetween(1, 32));
}
return Ints.toArray(days);
}
protected static Object randomDayOfMonth() {
int day = randomIntBetween(1, 32);
if (day == 32) {
return "last_day";
}
if (day == 1) {
return randomBoolean() ? "first_day" : 1;
}
return day;
}
protected static int dayOfMonthToInt(Object dom) {
if (dom instanceof Integer) {
return (Integer) dom;
}
if ("last_day".equals(dom)) {
return 32;
}
if ("first_day".equals(dom)) {
return 1;
}
throw new IllegalStateException("cannot convert given day-of-month [" + dom + "] to int");
}
protected static Object invalidDayOfMonth() {
return randomBoolean() ?
randomAsciiOfLength(5) :
randomBoolean() ? randomIntBetween(-30, -1) : randomIntBetween(33, 45);
}
protected static DayTimes validDayTime() {
return randomBoolean() ? DayTimes.parse(validDayTimeStr()) : new DayTimes(validHours(), validMinutes());
}
protected static String validDayTimeStr() {
int hour = validHour();
int min = validMinute();
StringBuilder sb = new StringBuilder();
if (hour < 10 && randomBoolean()) {
sb.append("0");
}
sb.append(hour).append(":");
if (min < 10) {
sb.append("0");
}
return sb.append(min).toString();
}
protected static HourAndMinute invalidDayTime() {
return randomBoolean() ?
new HourAndMinute(invalidHour(), invalidMinute()) :
randomBoolean() ?
new HourAndMinute(validHour(), invalidMinute()) :
new HourAndMinute(invalidHour(), validMinute());
}
protected static String invalidDayTimeStr() {
int hour;
int min;
switch (randomIntBetween(1, 3)) {
case 1:
hour = invalidHour();
min = validMinute();
break;
case 2:
hour = validHour();
min = invalidMinute();
break;
default:
hour = invalidHour();
min = invalidMinute();
}
StringBuilder sb = new StringBuilder();
if (hour < 10 && randomBoolean()) {
sb.append("0");
}
sb.append(hour).append(":");
if (min < 10) {
sb.append("0");
}
return sb.append(min).toString();
}
protected static DayTimes[] validDayTimes() {
int count = randomIntBetween(2, 5);
Set<DayTimes> times = new HashSet<>();
for (int i = 0; i < count; i++) {
times.add(validDayTime());
}
return times.toArray(new DayTimes[times.size()]);
}
protected static DayTimes[] validDayTimesFromNumbers() {
int count = randomIntBetween(2, 5);
Set<DayTimes> times = new HashSet<>();
for (int i = 0; i < count; i++) {
times.add(new DayTimes(validHours(), validMinutes()));
}
return times.toArray(new DayTimes[times.size()]);
}
protected static DayTimes[] validDayTimesFromStrings() {
int count = randomIntBetween(2, 5);
Set<DayTimes> times = new HashSet<>();
for (int i = 0; i < count; i++) {
times.add(DayTimes.parse(validDayTimeStr()));
}
return times.toArray(new DayTimes[times.size()]);
}
protected static HourAndMinute[] invalidDayTimes() {
int count = randomIntBetween(2, 5);
Set<HourAndMinute> times = new HashSet<>();
for (int i = 0; i < count; i++) {
times.add(invalidDayTime());
}
return times.toArray(new HourAndMinute[times.size()]);
}
protected static String[] invalidDayTimesAsStrings() {
int count = randomIntBetween(2, 5);
Set<String> times = new HashSet<>();
for (int i = 0; i < count; i++) {
times.add(invalidDayTimeStr());
}
return times.toArray(new String[times.size()]);
}
protected static int validMinute() {
return randomIntBetween(0, 59);
}
protected static int[] validMinutes() {
int count = randomIntBetween(2, 6);
int inc = 59 / count;
int[] minutes = new int[count];
for (int i = 0; i < count; i++) {
minutes[i] = randomIntBetween(i * inc, (i + 1) * inc);
}
return minutes;
}
protected static int invalidMinute() {
return randomBoolean() ? randomIntBetween(60, 100) : randomIntBetween(-60, -1);
}
protected static int[] invalidMinutes() {
int count = randomIntBetween(2, 6);
int[] minutes = new int[count];
for (int i = 0; i < count; i++) {
minutes[i] = invalidMinute();
}
return minutes;
}
protected static int validHour() {
return randomIntBetween(0, 23);
}
protected static int[] validHours() {
int count = randomIntBetween(2, 6);
int inc = 23 / count;
int[] hours = new int[count];
for (int i = 0; i < count; i++) {
hours[i] = randomIntBetween(i * inc, (i + 1) * inc);
}
return hours;
}
protected static int invalidHour() {
return randomBoolean() ? randomIntBetween(24, 40) : randomIntBetween(-60, -1);
}
static class HourAndMinute implements ToXContent {
int hour;
int minute;
public HourAndMinute(int hour, int minute) {
this.hour = hour;
this.minute = minute;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return builder.startObject()
.field(DayTimes.HOUR_FIELD.getPreferredName(), hour)
.field(DayTimes.MINUTE_FIELD.getPreferredName(), minute)
.endObject();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
HourAndMinute that = (HourAndMinute) o;
if (hour != that.hour) return false;
if (minute != that.minute) return false;
return true;
}
@Override
public int hashCode() {
int result = hour;
result = 31 * result + minute;
return result;
}
}
}

View File

@ -0,0 +1,145 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.scheduler.schedule;
import com.carrotsearch.randomizedtesting.annotations.Repeat;
import org.elasticsearch.alerts.AlertsSettingsException;
import org.elasticsearch.alerts.scheduler.schedule.support.DayTimes;
import org.elasticsearch.alerts.scheduler.schedule.support.DayOfWeek;
import org.elasticsearch.alerts.scheduler.schedule.support.WeekTimes;
import org.elasticsearch.common.base.Joiner;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.primitives.Ints;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.junit.Test;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.hamcrest.Matchers.*;
/**
*
*/
public class WeeklyScheduleTests extends ScheduleTestCase {
@Test
public void test_Default() throws Exception {
WeeklySchedule schedule = new WeeklySchedule();
String[] crons = schedule.crons();
assertThat(crons, arrayWithSize(1));
assertThat(crons, arrayContaining("0 0 0 ? * MON"));
}
@Test @Repeat(iterations = 20)
public void test_SingleTime() throws Exception {
WeekTimes time = validWeekTime();
WeeklySchedule schedule = new WeeklySchedule(time);
String[] crons = schedule.crons();
assertThat(crons, arrayWithSize(time.times().length));
for (DayTimes dayTimes : time.times()) {
assertThat(crons, hasItemInArray("0 " + Ints.join(",", dayTimes.minute()) + " " + Ints.join(",", dayTimes.hour()) + " ? * " + Joiner.on(",").join(time.days())));
}
}
@Test @Repeat(iterations = 20)
public void test_MultipleTimes() throws Exception {
WeekTimes[] times = validWeekTimes();
WeeklySchedule schedule = new WeeklySchedule(times);
String[] crons = schedule.crons();
int count = 0;
for (int i = 0; i < times.length; i++) {
count += times[i].times().length;
}
assertThat(crons, arrayWithSize(count));
for (WeekTimes weekTimes : times) {
for (DayTimes dayTimes : weekTimes.times()) {
assertThat(crons, hasItemInArray("0 " + Ints.join(",", dayTimes.minute()) + " " + Ints.join(",", dayTimes.hour()) + " ? * " + Joiner.on(",").join(weekTimes.days())));
}
}
}
@Test
public void testParser_Empty() throws Exception {
XContentBuilder builder = jsonBuilder().startObject().endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
WeeklySchedule schedule = new WeeklySchedule.Parser().parse(parser);
assertThat(schedule, notNullValue());
assertThat(schedule.times().length, is(1));
assertThat(schedule.times()[0], is(new WeekTimes(DayOfWeek.MONDAY, new DayTimes())));
}
@Test @Repeat(iterations = 20)
public void testParser_SingleTime() throws Exception {
DayTimes time = validDayTime();
XContentBuilder builder = jsonBuilder()
.startObject()
.field("on", "mon")
.startObject("at")
.field("hour", time.hour())
.field("minute", time.minute())
.endObject()
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
WeeklySchedule schedule = new WeeklySchedule.Parser().parse(parser);
assertThat(schedule, notNullValue());
assertThat(schedule.times().length, is(1));
assertThat(schedule.times()[0].days(), hasSize(1));
assertThat(schedule.times()[0].days(), contains(DayOfWeek.MONDAY));
assertThat(schedule.times()[0].times(), arrayWithSize(1));
assertThat(schedule.times()[0].times(), hasItemInArray(time));
}
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
public void testParser_SingleTime_Invalid() throws Exception {
HourAndMinute time = invalidDayTime();
XContentBuilder builder = jsonBuilder()
.startObject()
.field("on", "mon")
.startObject("at")
.field("hour", time.hour)
.field("minute", time.minute)
.endObject()
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
new WeeklySchedule.Parser().parse(parser);
}
@Test @Repeat(iterations = 20)
public void testParser_MultipleTimes() throws Exception {
WeekTimes[] times = validWeekTimes();
XContentBuilder builder = jsonBuilder().value(times);
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
WeeklySchedule schedule = new WeeklySchedule.Parser().parse(parser);
assertThat(schedule, notNullValue());
assertThat(schedule.times().length, is(times.length));
for (int i = 0; i < times.length; i++) {
assertThat(schedule.times(), hasItemInArray(times[i]));
}
}
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
public void testParser_MultipleTimes_Objects_Invalid() throws Exception {
HourAndMinute[] times = invalidDayTimes();
XContentBuilder builder = jsonBuilder()
.startObject()
.field("on", randomDaysOfWeek())
.array("at", (Object[]) times)
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
new WeeklySchedule.Parser().parse(parser);
}
}

View File

@ -0,0 +1,159 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.alerts.scheduler.schedule;
import com.carrotsearch.randomizedtesting.annotations.Repeat;
import org.elasticsearch.alerts.AlertsSettingsException;
import org.elasticsearch.alerts.scheduler.schedule.support.DayTimes;
import org.elasticsearch.alerts.scheduler.schedule.support.YearTimes;
import org.elasticsearch.common.base.Joiner;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.primitives.Ints;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.junit.Test;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.hamcrest.Matchers.*;
/**
*
*/
public class YearlyScheduleTests extends ScheduleTestCase {
@Test
public void test_Default() throws Exception {
YearlySchedule schedule = new YearlySchedule();
String[] crons = schedule.crons();
assertThat(crons, arrayWithSize(1));
assertThat(crons, arrayContaining("0 0 0 1 JAN ?"));
}
@Test @Repeat(iterations = 20)
public void test_SingleTime() throws Exception {
YearTimes time = validYearTime();
YearlySchedule schedule = new YearlySchedule(time);
String[] crons = schedule.crons();
assertThat(crons, arrayWithSize(time.times().length));
for (DayTimes dayTimes : time.times()) {
String minStr = Ints.join(",", dayTimes.minute());
String hrStr = Ints.join(",", dayTimes.hour());
String dayStr = Ints.join(",", time.days());
dayStr = dayStr.replace("32", "L");
String monthStr = Joiner.on(",").join(time.months());
assertThat(crons, hasItemInArray("0 " + minStr + " " + hrStr + " " + dayStr + " " + monthStr + " ?"));
}
}
@Test @Repeat(iterations = 20)
public void test_MultipleTimes() throws Exception {
YearTimes[] times = validYearTimes();
YearlySchedule schedule = new YearlySchedule(times);
String[] crons = schedule.crons();
int count = 0;
for (int i = 0; i < times.length; i++) {
count += times[i].times().length;
}
assertThat(crons, arrayWithSize(count));
for (YearTimes yearTimes : times) {
for (DayTimes dayTimes : yearTimes.times()) {
String minStr = Ints.join(",", dayTimes.minute());
String hrStr = Ints.join(",", dayTimes.hour());
String dayStr = Ints.join(",", yearTimes.days());
dayStr = dayStr.replace("32", "L");
String monthStr = Joiner.on(",").join(yearTimes.months());
assertThat(crons, hasItemInArray("0 " + minStr + " " + hrStr + " " + dayStr + " " + monthStr + " ?"));
}
}
}
@Test @Repeat(iterations = 20)
public void testParser_Empty() throws Exception {
XContentBuilder builder = jsonBuilder().startObject().endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
YearlySchedule schedule = new YearlySchedule.Parser().parse(parser);
assertThat(schedule, notNullValue());
assertThat(schedule.times().length, is(1));
assertThat(schedule.times()[0], is(new YearTimes()));
}
@Test @Repeat(iterations = 20)
public void testParser_SingleTime() throws Exception {
DayTimes time = validDayTime();
Object day = randomDayOfMonth();
Object month = randomMonth();
XContentBuilder builder = jsonBuilder()
.startObject()
.field("in", month)
.field("on", day)
.startObject("at")
.field("hour", time.hour())
.field("minute", time.minute())
.endObject()
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
YearlySchedule schedule = new YearlySchedule.Parser().parse(parser);
assertThat(schedule, notNullValue());
assertThat(schedule.times().length, is(1));
assertThat(schedule.times()[0].days().length, is(1));
assertThat(schedule.times()[0].days()[0], is(dayOfMonthToInt(day)));
assertThat(schedule.times()[0].times(), arrayWithSize(1));
assertThat(schedule.times()[0].times(), hasItemInArray(time));
}
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
public void testParser_SingleTime_Invalid() throws Exception {
HourAndMinute time = invalidDayTime();
XContentBuilder builder = jsonBuilder()
.startObject()
.field("in", randomMonth())
.field("on", randomBoolean() ? invalidDayOfMonth() : randomDayOfMonth())
.startObject("at")
.field("hour", time.hour)
.field("minute", time.minute)
.endObject()
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
new MonthlySchedule.Parser().parse(parser);
}
@Test @Repeat(iterations = 20)
public void testParser_MultipleTimes() throws Exception {
YearTimes[] times = validYearTimes();
XContentBuilder builder = jsonBuilder().value(times);
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
YearlySchedule schedule = new YearlySchedule.Parser().parse(parser);
assertThat(schedule, notNullValue());
assertThat(schedule.times().length, is(times.length));
for (YearTimes time : times) {
assertThat(schedule.times(), hasItemInArray(time));
}
}
@Test(expected = AlertsSettingsException.class) @Repeat(iterations = 20)
public void testParser_MultipleTimes_Invalid() throws Exception {
HourAndMinute[] times = invalidDayTimes();
XContentBuilder builder = jsonBuilder()
.startObject()
.field("in", randomMonth())
.field("on", randomDayOfMonth())
.array("at", (Object[]) times)
.endObject();
BytesReference bytes = builder.bytes();
XContentParser parser = JsonXContent.jsonXContent.createParser(bytes);
parser.nextToken(); // advancing to the start object
new YearlySchedule.Parser().parse(parser);
}
}