Java tutorial
/* * Copyright 2013-2014 Classmethod, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * 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 jp.classmethod.aws.brian.web; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TimeZone; import java.util.stream.Collectors; import jp.classmethod.aws.brian.model.BrianTrigger; import jp.classmethod.aws.brian.model.BrianTriggerRequest; import jp.classmethod.aws.brian.utils.BrianFactory; import jp.xet.baseunits.time.CalendarUtil; import jp.xet.baseunits.time.TimePoint; import jp.xet.baseunits.util.TimeZones; import com.amazonaws.services.sns.AmazonSNS; import com.google.common.base.Strings; import com.google.gson.Gson; import org.quartz.CronExpression; import org.quartz.CronScheduleBuilder; import org.quartz.JobDataMap; import org.quartz.JobDetail; import org.quartz.ScheduleBuilder; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.SimpleScheduleBuilder; import org.quartz.SimpleTrigger; import org.quartz.Trigger; import org.quartz.TriggerBuilder; import org.quartz.TriggerKey; import org.quartz.impl.matchers.GroupMatcher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; /** * Controller implementation to operate trigger groups and triggers. * * @since 1.0 * @author daisuke */ @Controller @SuppressWarnings("javadoc") public class TriggerController { private static Logger logger = LoggerFactory.getLogger(TriggerController.class); @Autowired Gson gson; @Autowired Scheduler scheduler; @Autowired @Qualifier("brianQuartzJob") JobDetail quartzJob; @Autowired AmazonSNS sns; @Value("#{systemProperties['BRIAN_TOPIC_ARN']}") String topicArn; /** * Get trigger groups. * * @return trigger groups * @throws SchedulerException */ @ResponseBody @RequestMapping(value = "/triggers", method = RequestMethod.GET) public List<String> listTriggerGroups() throws SchedulerException { logger.info("getTriggerGroups"); return scheduler.getTriggerGroupNames().stream().sorted().peek(name -> logger.info(" group {}", name)) .collect(Collectors.toList()); } /** * Get trigger names in the specified group. * * @param triggerGroupName groupName * @return trigger names * @throws SchedulerException */ @ResponseBody @RequestMapping(value = "/triggers/{triggerGroupName}", method = RequestMethod.GET) public List<String> listTriggers(@PathVariable("triggerGroupName") String triggerGroupName) throws SchedulerException { logger.info("getTriggerNames {}", triggerGroupName); return scheduler.getTriggerKeys(GroupMatcher.triggerGroupEquals(triggerGroupName)).stream() .map(triggerKey -> triggerKey.getName()).sorted().peek(name -> logger.info(" trigger {}", name)) .collect(Collectors.toList()); } /** * Delete specified triggerGroup (belonging triggers). * * @param triggerGroupName trigger group name * @return wherther the trigger is removed * @throws SchedulerException */ @ResponseBody @RequestMapping(value = "/triggers/{triggerGroupName}/", method = RequestMethod.DELETE) public ResponseEntity<?> deleteTriggerGroup(@PathVariable("triggerGroupName") String triggerGroupName) throws SchedulerException { logger.info("deleteTriggerGroup {}", triggerGroupName); List<String> triggers = listTriggers(triggerGroupName); Set<String> failed = triggers.stream().filter(triggerName -> { TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroupName); try { return scheduler.unscheduleJob(triggerKey) == false; } catch (SchedulerException e) { logger.error("unexpected", e); } return false; }).collect(Collectors.toSet()); if (failed.isEmpty()) { return ResponseEntity.ok(triggers); } Map<String, Object> map = new HashMap<>(); map.put("failed", failed); map.put("deleted", triggers.stream().filter(t -> failed.contains(t) == false).collect(Collectors.toList())); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(new BrianResponse<>(false, "failed to unschedule", map)); } /** * Create trigger. * * @param triggerGroupName groupName * @return trigger names * @throws SchedulerException */ @ResponseBody @RequestMapping(value = "/triggers/{triggerGroupName}", method = RequestMethod.POST) public ResponseEntity<?> createTrigger(@PathVariable("triggerGroupName") String triggerGroupName, @RequestBody BrianTriggerRequest triggerRequest) throws SchedulerException { logger.info("createTrigger {}.{}", triggerGroupName, triggerRequest.getTriggerName()); logger.info("{}", triggerRequest); String triggerName = triggerRequest.getTriggerName(); if (Strings.isNullOrEmpty(triggerName)) { return new ResponseEntity<>(new BrianResponse<>(false, "triggerName is not found"), HttpStatus.BAD_REQUEST); } try { TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroupName); if (scheduler.checkExists(triggerKey)) { String message = String.format("trigger %s.%s already exists.", triggerGroupName, triggerName); return new ResponseEntity<>(new BrianResponse<>(false, message), HttpStatus.CONFLICT); } Trigger trigger = getTrigger(triggerRequest, triggerKey); Date nextFireTime = scheduler.scheduleJob(trigger); logger.info("scheduled {}", triggerKey); SimpleDateFormat df = CalendarUtil.newSimpleDateFormat(TimePoint.ISO8601_FORMAT_UNIVERSAL, Locale.US, TimeZones.UNIVERSAL); Map<String, Object> map = new HashMap<>(); map.put("nextFireTime", df.format(nextFireTime)); return new ResponseEntity<>(new BrianResponse<>(true, "created", map), HttpStatus.CREATED); // TODO return URI } catch (ParseException e) { logger.warn("parse cron expression failed", e); String message = "parse cron expression failed - " + e.getMessage(); return ResponseEntity.badRequest().body(new BrianResponse<>(false, message)); } } /** * Update the trigger. * * @param triggerGroupName trigger group name * @param triggerName trigger name * @param triggerRequest triggerRequest * @return {@link HttpStatus#CREATED} if the trigger is created, {@link HttpStatus#OK} if the trigger is updated. * And nextFireTime property which is represent that the trigger's next fire time. * @throws SchedulerException */ @ResponseBody @RequestMapping(value = "/triggers/{triggerGroupName}/{triggerName}", method = RequestMethod.PUT) public ResponseEntity<?> updateTrigger(@PathVariable("triggerGroupName") String triggerGroupName, @PathVariable("triggerName") String triggerName, @RequestBody BrianTriggerRequest triggerRequest) throws SchedulerException { logger.info("updateTrigger {}.{}: {}", triggerGroupName, triggerName, triggerRequest); if (triggerName.equals(triggerRequest.getTriggerName()) == false) { String message = String.format( "trigger names '%s' in the path and '%s' in the request body is not equal", triggerName, triggerRequest.getTriggerName()); return ResponseEntity.badRequest().body(new BrianResponse<>(false, message)); } try { TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroupName); if (scheduler.checkExists(triggerKey) == false) { String message = String.format("trigger %s.%s is not found.", triggerGroupName, triggerName); return new ResponseEntity<>(new BrianResponse<>(false, message), HttpStatus.NOT_FOUND); } Trigger trigger = getTrigger(triggerRequest, triggerKey); Date nextFireTime = scheduler.rescheduleJob(triggerKey, trigger); logger.info("rescheduled {}", triggerKey); Map<String, Object> map = new HashMap<>(); map.put("nextFireTime", new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).format(nextFireTime)); return ResponseEntity.ok(new BrianResponse<>(true, "ok", map)); } catch (ParseException e) { logger.warn("parse cron expression failed", e); String message = "parse cron expression failed - " + e.getMessage(); return ResponseEntity.badRequest().body(new BrianResponse<>(false, message)); } } /** * Get trigger information for the specified trigger. * * @param triggerGroupName trigger group name * @param triggerName trigger name * @return trigger information * @throws SchedulerException * @throws ResourceNotFoundException */ @ResponseBody @RequestMapping(value = "/triggers/{triggerGroupName}/{triggerName}", method = RequestMethod.GET, produces = "application/json") public ResponseEntity<?> describeTrigger(@PathVariable("triggerGroupName") String triggerGroupName, @PathVariable("triggerName") String triggerName) throws SchedulerException { logger.info("getTrigger {}.{}", triggerGroupName, triggerName); TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroupName); Trigger trigger = scheduler.getTrigger(triggerKey); logger.info("quartz trigger = {}", trigger); if (trigger == null) { throw new ResourceNotFoundException(); } BrianTrigger brianTrigger = BrianFactory.createBrianTrigger(trigger); logger.info("brian trigger = {}", trigger); return ResponseEntity.ok(brianTrigger); } /** * Delete specified trigger. * * @param triggerGroupName trigger group name * @param triggerName trigger name * @return wherther the trigger is removed * @throws SchedulerException */ @ResponseBody @RequestMapping(value = "/triggers/{triggerGroupName}/{triggerName}", method = RequestMethod.DELETE) public ResponseEntity<?> deleteTrigger(@PathVariable("triggerGroupName") String triggerGroupName, @PathVariable("triggerName") String triggerName) throws SchedulerException { logger.info("deleteTrigger {}.{}", triggerGroupName, triggerName); TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroupName); if (scheduler.checkExists(triggerKey) == false) { return ResponseEntity.notFound().build(); } boolean deleted = scheduler.unscheduleJob(triggerKey); if (deleted) { return ResponseEntity.ok(new BrianResponse<>(true, "ok")); } else { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(new BrianResponse<>(false, "unschedule failed")); } } @ResponseBody @RequestMapping(value = "/triggers/{triggerGroupName}/{triggerName}", method = RequestMethod.POST) public ResponseEntity<?> forceFireTrigger(@PathVariable("triggerGroupName") String triggerGroupName, @PathVariable("triggerName") String triggerName) throws SchedulerException { logger.info("forceFireTrigger {}.{}", triggerGroupName, triggerName); TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroupName); if (scheduler.checkExists(triggerKey) == false) { String message = String.format("trigger %s.%s is not found.", triggerGroupName, triggerName); return new ResponseEntity<>(new BrianResponse<>(false, message), HttpStatus.NOT_FOUND); } Trigger trigger = scheduler.getTrigger(triggerKey); scheduler.triggerJob(quartzJob.getKey(), trigger.getJobDataMap()); Map<String, Object> map = new HashMap<>(); return ResponseEntity.ok(new BrianResponse<>(true, "ok", map)); } private Trigger getTrigger(BrianTriggerRequest triggerRequest, TriggerKey triggerKey) throws ParseException { ScheduleBuilder<? extends Trigger> schedule = getSchedule(triggerRequest); TriggerBuilder<? extends Trigger> tb = TriggerBuilder.newTrigger().forJob(quartzJob) .withIdentity(triggerKey).withSchedule(schedule).withPriority(triggerRequest.getPriority()) .withDescription(triggerRequest.getDescription()); if (triggerRequest.getJobData() != null) { logger.debug("job data map = {}", triggerRequest.getJobData()); tb.usingJobData(new JobDataMap(triggerRequest.getJobData())); } if (triggerRequest.getStartAt() != null) { tb.startAt(triggerRequest.getStartAt()); } else { tb.startNow(); } if (triggerRequest.getEndAt() != null) { tb.endAt(triggerRequest.getEndAt()); } // TODO time validation Trigger trigger = tb.build(); return trigger; } @ResponseBody @RequestMapping(value = "/scheduler/resume", method = RequestMethod.PUT) public ResponseEntity<?> resumeScheduler() throws SchedulerException { scheduler.resumeAll(); logger.info("resumeAll"); return ResponseEntity.ok(new BrianResponse<>(true, "ok")); } @ResponseBody @RequestMapping(value = "/scheduler/pause", method = RequestMethod.PUT) public ResponseEntity<?> pauseScheduler() throws SchedulerException { scheduler.pauseAll(); logger.info("pauseAll"); return ResponseEntity.ok(new BrianResponse<>(true, "ok")); } @ResponseBody @RequestMapping(value = "/scheduler/start", method = RequestMethod.PUT) public ResponseEntity<?> startScheduler() throws SchedulerException { scheduler.start(); logger.info("start"); return ResponseEntity.ok(new BrianResponse<>(true, "ok")); } @ResponseBody @RequestMapping(value = "/scheduler/standby", method = RequestMethod.PUT) public ResponseEntity<?> standbyScheduler() throws SchedulerException { scheduler.standby(); logger.info("standby"); return ResponseEntity.ok(new BrianResponse<>(true, "ok")); } private ScheduleBuilder<? extends Trigger> getSchedule(BrianTriggerRequest triggerRequest) throws ParseException { switch (triggerRequest.getScheduleType()) { case oneshot: return createOneShotSchedule(triggerRequest); case simple: return createSimpleSchedule(triggerRequest); case cron: default: return createCronSchedule(triggerRequest); } } private CronScheduleBuilder createCronSchedule(BrianTriggerRequest triggerRequest) throws ParseException { String cronExString = triggerRequest.getRest().get("cronEx"); if (cronExString == null) { throw new IllegalArgumentException("cronEx is null"); } CronExpression cronExpression = new CronExpression(cronExString); String timeZoneId = triggerRequest.getRest().get("timeZone"); if (timeZoneId != null) { cronExpression.setTimeZone(TimeZone.getTimeZone(timeZoneId)); } CronScheduleBuilder cronSchedule = CronScheduleBuilder.cronSchedule(cronExpression); if (triggerRequest.getMisfireInstruction() != null) { switch (triggerRequest.getMisfireInstruction()) { case "IGNORE": case "IGNORE_MISFIRE_POLICY": return cronSchedule.withMisfireHandlingInstructionIgnoreMisfires(); case "FIRE_ONCE_NOW": return cronSchedule.withMisfireHandlingInstructionFireAndProceed(); case "DO_NOTHING": return cronSchedule.withMisfireHandlingInstructionDoNothing(); default: case "SMART_POLICY": // do nothing } } return cronSchedule; } private SimpleScheduleBuilder createSimpleSchedule(BrianTriggerRequest triggerRequest) { long repeatInterval; int repeatCount; try { repeatInterval = Long.valueOf(triggerRequest.getRest().get("repeatInterval")); try { repeatCount = Integer.valueOf(triggerRequest.getRest().get("repeatCount")); } catch (NumberFormatException e) { repeatCount = SimpleTrigger.REPEAT_INDEFINITELY; } } catch (NumberFormatException e) { repeatInterval = 0; repeatCount = 1; } SimpleScheduleBuilder simpleSchedule = SimpleScheduleBuilder.simpleSchedule() .withIntervalInMilliseconds(repeatInterval).withRepeatCount(repeatCount); if (triggerRequest.getMisfireInstruction() != null) { switch (triggerRequest.getMisfireInstruction()) { case "IGNORE": case "IGNORE_MISFIRE_POLICY": return simpleSchedule.withMisfireHandlingInstructionIgnoreMisfires(); case "FIRE_NOW": return simpleSchedule.withMisfireHandlingInstructionFireNow(); case "RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT": return simpleSchedule.withMisfireHandlingInstructionNowWithExistingCount(); case "RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT": return simpleSchedule.withMisfireHandlingInstructionNowWithRemainingCount(); case "RESCHEDULE_NEXT_WITH_REMAINING_COUNT": return simpleSchedule.withMisfireHandlingInstructionNextWithRemainingCount(); case "RESCHEDULE_NEXT_WITH_EXISTING_COUNT": return simpleSchedule.withMisfireHandlingInstructionNextWithExistingCount(); default: case "SMART_POLICY": // do nothing } } return simpleSchedule; } private SimpleScheduleBuilder createOneShotSchedule(BrianTriggerRequest triggerRequest) { SimpleScheduleBuilder oneShotSchedule = SimpleScheduleBuilder.simpleSchedule(); if (triggerRequest.getMisfireInstruction() != null) { switch (triggerRequest.getMisfireInstruction()) { case "IGNORE": case "IGNORE_MISFIRE_POLICY": return oneShotSchedule.withMisfireHandlingInstructionIgnoreMisfires(); case "FIRE_NOW": return oneShotSchedule.withMisfireHandlingInstructionFireNow(); default: case "SMART_POLICY": // do nothing } } return oneShotSchedule; } }