/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.jooby.internal.quartz;
import static com.google.common.base.Preconditions.checkArgument;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import org.jooby.quartz.Scheduled;
import org.quartz.CronScheduleBuilder;
import org.quartz.Job;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobKey;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.quartz.impl.JobDetailImpl;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigValueFactory;
public class JobExpander {
@SuppressWarnings("unchecked")
public static Map<JobDetail, Trigger> jobs(final Config config, final List<Class<?>> jobs) {
Map<JobDetail, Trigger> triggers = new HashMap<>();
for (Class<?> job : jobs) {
if (Job.class.isAssignableFrom(job)) {
triggers.put(
job((Class<? extends Job>) job),
trigger(config, (Class<? extends Job>) job)
);
} else {
Method[] methods = job.getDeclaredMethods();
int size = triggers.size();
for (Method method : methods) {
Scheduled scheduled = method.getAnnotation(Scheduled.class);
if (scheduled != null) {
int mods = method.getModifiers();
if (!Modifier.isPublic(mods)) {
throw new IllegalArgumentException("Job method must be public: " + method);
}
if (Modifier.isStatic(mods)) {
throw new IllegalArgumentException("Job method should NOT be public: " + method);
}
if (method.getParameterCount() > 0) {
if (method.getParameterCount() > 1) {
throw new IllegalArgumentException("Job method args must be ZERO/ONE: "
+ method);
}
if (method.getParameterTypes()[0] != JobExecutionContext.class) {
throw new IllegalArgumentException("Job method args isn't a "
+ JobExecutionContext.class.getName() + ": " + method);
}
}
triggers.put(job(method), newTrigger(config, scheduled, jobKey(method)));
}
}
checkArgument(size < triggers.size(), "Scheduled is missing on %s", job.getName());
}
}
return triggers;
}
private static JobDetail job(final Class<? extends Job> jobType) {
JobKey key = jobKey(jobType);
return JobBuilder.newJob(jobType)
.withIdentity(key)
.build();
}
private static JobDetail job(final Method method) {
JobDetailImpl detail = new MethodJobDetail(method);
detail.setJobClass(ReflectiveJob.class);
detail.setKey(jobKey(method));
return detail;
}
private static JobKey jobKey(final Class<?> jobType) {
return JobKey.jobKey(jobType.getSimpleName(), jobType.getPackage().getName());
}
private static JobKey jobKey(final Method method) {
Class<?> klass = method.getDeclaringClass();
String classname = klass.getSimpleName();
klass = klass.getDeclaringClass();
while (klass != null) {
classname = klass.getSimpleName() + "$" + classname;
klass = klass.getDeclaringClass();
}
return JobKey.jobKey(classname + "." + method.getName(),
method.getDeclaringClass().getPackage().getName());
}
private static Trigger trigger(final Config config, final Class<? extends Job> jobType) {
Method execute = Arrays.stream(jobType.getDeclaredMethods())
.filter(m -> m.getName().equals("execute"))
.findFirst()
.get();
Scheduled scheduled = execute.getAnnotation(Scheduled.class);
checkArgument(scheduled != null, "Scheduled is missing on %s.%s()", jobType.getName(),
execute.getName());
return newTrigger(config, scheduled, jobKey(jobType));
}
private static Trigger newTrigger(final Config config, final Scheduled scheduled,
final JobKey key) {
String expr = scheduled.value();
// hack
Object value = eval(key, config, expr);
// almost there
if (value instanceof String) {
// cron
return TriggerBuilder.newTrigger()
.withSchedule(
CronScheduleBuilder
.cronSchedule((String) value)
)
.withIdentity(TriggerKey.triggerKey(key.getName(), key.getGroup()))
.build();
} else {
Long[] interval = (Long[]) value;
SimpleScheduleBuilder sb = SimpleScheduleBuilder
.simpleSchedule()
.withIntervalInMilliseconds(interval[0]);
if (interval[2] > 0) {
sb = sb.withRepeatCount(interval[2].intValue());
} else {
sb = sb.repeatForever();
}
return TriggerBuilder.newTrigger()
.withSchedule(sb)
.withIdentity(TriggerKey.triggerKey(key.getName(), key.getGroup()))
.startAt(new Date(System.currentTimeMillis() + interval[1]))
.build();
}
}
private static Object eval(final JobKey key, final Config config, final String expr) {
// full expression with possible delay and repeat values
return eval(config, expr, (values, resolved) -> {
if (resolved instanceof Long) {
// interval with delay and repeat
Long[] inverval = new Long[]{(Long) resolved, 0L, 0L };
for (int i = 1; i < values.length; i++) {
String[] attr = values[i].split("=");
if ("delay".equals(attr[0].trim())) {
inverval[1] = (Long) eval(config, attr[1], (v, r) -> r);
} else if ("repeat".equals(attr[0].trim())) {
if (!"*".equals(attr[1].trim())) {
inverval[2] = (Long) eval(config, attr[1], (v, r) -> r);
}
} else {
throw new IllegalArgumentException("Unknown attribute: " + attr[0] + " at " + key);
}
}
return inverval;
}
return resolved;
});
}
private static Object eval(final Config config, final String expr,
final BiFunction<String[], Object, Object> mapper) {
String value = expr.trim();
try {
value = config.getString(value);
} catch (ConfigException.BadPath | ConfigException.Missing ex) {
// shh
}
String[] values = value.split(";");
Config eval = ConfigFactory.empty()
.withValue("expr", ConfigValueFactory.fromAnyRef(values[0]));
try {
return mapper.apply(values, eval.getDuration("expr", TimeUnit.MILLISECONDS));
} catch (ConfigException.WrongType | ConfigException.BadValue ex) {
return mapper.apply(values, value);
}
}
}