com.whizzosoftware.hobson.scheduler.ical.ICalTaskProvider.java Source code

Java tutorial

Introduction

Here is the source code for com.whizzosoftware.hobson.scheduler.ical.ICalTaskProvider.java

Source

/*******************************************************************************
 * Copyright (c) 2014 Whizzo Software, LLC.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *******************************************************************************/
package com.whizzosoftware.hobson.scheduler.ical;

import com.whizzosoftware.hobson.api.HobsonNotFoundException;
import com.whizzosoftware.hobson.api.HobsonRuntimeException;
import com.whizzosoftware.hobson.api.action.ActionManager;
import com.whizzosoftware.hobson.api.task.HobsonTask;
import com.whizzosoftware.hobson.api.task.TaskProvider;
import com.whizzosoftware.hobson.api.util.filewatch.FileWatcherListener;
import com.whizzosoftware.hobson.api.util.filewatch.FileWatcherThread;
import com.whizzosoftware.hobson.scheduler.DayResetListener;
import com.whizzosoftware.hobson.scheduler.TaskExecutionListener;
import com.whizzosoftware.hobson.scheduler.executor.ScheduledTaskExecutor;
import com.whizzosoftware.hobson.scheduler.util.DateHelper;
import net.fortuna.ical4j.data.CalendarBuilder;
import net.fortuna.ical4j.data.CalendarOutputter;
import net.fortuna.ical4j.model.*;
import net.fortuna.ical4j.model.Calendar;
import net.fortuna.ical4j.model.Date;
import net.fortuna.ical4j.model.component.VEvent;
import net.fortuna.ical4j.model.component.VJournal;
import net.fortuna.ical4j.model.property.CalScale;
import net.fortuna.ical4j.model.property.ProdId;
import net.fortuna.ical4j.model.property.Uid;
import net.fortuna.ical4j.model.property.Version;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.util.*;
import java.util.TimeZone;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * A Scheduler implementation that uses the iCal (RFC 5445) format.
 *
 * @author Dan Noguerol
 */
public class ICalTaskProvider implements TaskProvider, FileWatcherListener, TaskExecutionListener {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    public static final String PROVIDER = "com.whizzosoftware.hobson.hub.hobson-hub-scheduler";

    private static final long MS_24_HOURS = 86400000;

    private DayResetListener dayResetListener;
    private String pluginId;
    private ActionManager actionManager;
    private Calendar calendar;
    private ScheduledTaskExecutor executor;
    private final Map<String, ICalTask> taskMap = new HashMap<>();
    private File scheduleFile;
    private FileWatcherThread watcherThread;
    private ScheduledThreadPoolExecutor resetDayExecutor;
    private Double latitude;
    private Double longitude;
    private TimeZone timeZone;
    private boolean running = false;

    public ICalTaskProvider(String pluginId, Double latitude, Double longitude) {
        this(pluginId, latitude, longitude, TimeZone.getDefault());
    }

    public ICalTaskProvider(String pluginId, Double latitude, Double longitude, TimeZone timeZone) {
        this.pluginId = pluginId;
        this.latitude = latitude;
        this.longitude = longitude;
        this.timeZone = timeZone;
    }

    public void setDayResetListener(DayResetListener dayResetListener) {
        this.dayResetListener = dayResetListener;
    }

    @Override
    public String getPluginId() {
        return pluginId;
    }

    @Override
    public String getId() {
        return PROVIDER;
    }

    public Double getLatitude() {
        return latitude;
    }

    public Double getLongitude() {
        return longitude;
    }

    public void setLatitudeLongitude(Double latitude, Double longitude) {
        this.latitude = latitude;
        this.longitude = longitude;
        reloadScheduleFile();
    }

    @Override
    synchronized public Collection<HobsonTask> getTasks() {
        return new ArrayList<HobsonTask>(taskMap.values());
    }

    @Override
    synchronized public HobsonTask getTask(String taskId) {
        return taskMap.get(taskId);
    }

    @Override
    synchronized public void addTask(Object task) {
        try {
            JSONObject json = (JSONObject) task;
            ICalTask ict = new ICalTask(actionManager, PROVIDER, json);
            ict.setLatitude(latitude);
            ict.setLongitude(longitude);
            calendar.getComponents().add(ict.getVEvent());
            writeFile();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Adds a new task.
     *
     * @param task the task to add
     * @param now the current time
     * @param wasDayReset indicates whether this is being done as part of a new day reset
     *
     * @return a boolean indicating whether the task should be run immediately
     *
     * @throws Exception on failure
     */
    protected boolean addTask(ICalTask task, long now, boolean wasDayReset) throws Exception {
        logger.debug("Adding task {} with ID: {}", task.getName(), task.getId());
        taskMap.put(task.getId(), task);
        return scheduleNextRun(task, now, wasDayReset);
    }

    /**
     * Schedules the next run of a task.
     *
     * @param task the task to schedule
     * @param now the current time
     * @param wasDayReset indicates whether this is being done as part of a new day reset
     *
     * @return a boolean indicating whether the task should be run immediately
     *
     * @throws Exception on failure
     */
    protected boolean scheduleNextRun(ICalTask task, long now, boolean wasDayReset) throws Exception {
        long startOfToday = DateHelper.getTimeInCurrentDay(now, timeZone, 0, 0, 0, 0).getTimeInMillis();
        long endOfToday = DateHelper.getTimeInCurrentDay(now, timeZone, 23, 59, 59, 999).getTimeInMillis();
        boolean shouldRunToday = false;

        task.getProperties().put(ICalTask.PROP_SCHEDULED, false);

        try {
            // check if there is more than 1 run in the next two days
            List<Long> todaysRunTimes = task.getRunsDuringInterval(startOfToday, startOfToday + 86400000l);
            // if not, check if there is more than 1 run in the next 6 weeks
            if (todaysRunTimes.size() < 2) {
                todaysRunTimes = task.getRunsDuringInterval(startOfToday, startOfToday + 3628800000l);
                // it not, check if there is more than 1 run in the next 53 weeks
                if (todaysRunTimes.size() < 2) {
                    todaysRunTimes = task.getRunsDuringInterval(startOfToday, startOfToday + 32054400000l);
                }
            }

            if (todaysRunTimes.size() > 0) {
                long nextRunTime = 0;
                for (Long l : todaysRunTimes) {
                    if (l - now < 0 && wasDayReset) {
                        shouldRunToday = true;
                    } else if (l - now > 0) {
                        nextRunTime = l;
                        break;
                    }
                }
                if (nextRunTime > 0) {
                    task.getProperties().put(ICalTask.PROP_NEXT_RUN_TIME, nextRunTime);
                    if (nextRunTime < endOfToday) {
                        task.getProperties().put(ICalTask.PROP_SCHEDULED, true);
                        executor.schedule(task, nextRunTime - now);
                    }
                }
            }
        } catch (SchedulingException e) {
            task.getProperties().put(ICalTask.PROP_ERROR, e.getLocalizedMessage());
        }

        return shouldRunToday;
    }

    @Override
    synchronized public void updateTask(String taskId, Object task) {
        try {
            // create new ical task
            JSONObject json = (JSONObject) task;
            json.put("id", taskId);
            ICalTask newTask = new ICalTask(actionManager, PROVIDER, json);
            newTask.setLatitude(latitude);
            newTask.setLongitude(longitude);

            ICalTask oldTask = taskMap.get(taskId);
            if (oldTask != null) {
                // remove old task
                calendar.getComponents().remove(oldTask.getVEvent());
                // add new task
                calendar.getComponents().add(newTask.getVEvent());
                taskMap.put(taskId, newTask);
                writeFile();
            } else {
                throw new HobsonNotFoundException("The specified task was not found");
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    synchronized public void deleteTask(String taskId) {
        ICalTask task = taskMap.get(taskId);
        if (task != null) {
            // remove the task from the calendar and write out a new calendar file
            calendar.getComponents().remove(task.getVEvent());
            taskMap.remove(taskId);
            writeFile();
        } else {
            logger.error("Unable to find task for removal: {}", taskId);
        }
    }

    public void setScheduleFile(File scheduleFile) {
        this.scheduleFile = scheduleFile;

        if (running) {
            restartFileWatcher();
            reloadScheduleFile();
        }
    }

    public void reloadScheduleFile() {
        if (scheduleFile != null) {
            try {
                // create empty calendar file if it doesn't exist
                if (!scheduleFile.exists()) {
                    Calendar calendar = new Calendar();
                    calendar.getProperties().add(new ProdId("-//Whizzo Software//Hobson 1.0//EN"));
                    calendar.getProperties().add(Version.VERSION_2_0);
                    calendar.getProperties().add(CalScale.GREGORIAN);
                    VJournal entry = new VJournal(new Date(), "Created");
                    entry.getProperties().add(new Uid(UUID.randomUUID().toString()));
                    calendar.getComponents().add(entry);
                    CalendarOutputter outputter = new CalendarOutputter();
                    outputter.output(calendar, new FileOutputStream(scheduleFile));
                }

                // load the calendar file
                logger.debug("Scheduler loading file: {}", scheduleFile.getAbsolutePath());
                loadICSStream(new FileInputStream(scheduleFile), System.currentTimeMillis());
            } catch (Exception e) {
                throw new HobsonRuntimeException("Error setting schedule file", e);
            }
        }
    }

    public void setScheduleExecutor(ScheduledTaskExecutor executor) {
        this.executor = executor;
    }

    public void setActionManager(ActionManager actionManager) {
        this.actionManager = actionManager;
    }

    public void start() {
        if (!running) {
            long initialDelay = DateHelper.getMillisecondsUntilMidnight(System.currentTimeMillis(), timeZone);

            executor.start();

            restartFileWatcher();
            reloadScheduleFile();

            logger.debug("New day will start in {} seconds", (initialDelay / 1000));
            resetDayExecutor = new ScheduledThreadPoolExecutor(1);
            resetDayExecutor.scheduleAtFixedRate(new Runnable() {
                @Override
                public void run() {
                    resetForNewDay(System.currentTimeMillis());
                }
            }, initialDelay, MS_24_HOURS, TimeUnit.MILLISECONDS);

            running = true;
        }
    }

    public void stop() {
        running = false;

        stopFileWatcher();

        resetDayExecutor.shutdownNow();
        resetDayExecutor = null;

        executor.stop();
        executor = null;

        taskMap.clear();
    }

    public void resetForNewDay(long now) {
        // alert listener
        if (dayResetListener != null) {
            dayResetListener.onDayReset(now);
        }

        // refresh the internal calendar data to identify new tasks that should be scheduled
        try {
            refreshLocalCalendarData(now, true);
        } catch (Exception e) {
            logger.error("Error reloading calendar file on day reset", e);
        }
    }

    @Override
    public void onFileChanged(File ruleFile) {
        try {
            logger.debug("Detected calendar file change");
            // load and parse the new calendar file
            loadICSStream(new FileInputStream(ruleFile), System.currentTimeMillis());
        } catch (FileNotFoundException e) {
            logger.error("Unable to find schedule file at " + scheduleFile.getAbsolutePath(), e);
        } catch (Exception e) {
            logger.error("Error loading calendar file; will continue to use previous one", e);
        }
    }

    @Override
    public void onTaskExecuted(ICalTask task, long now) {
        onTaskExecuted(task, now, false);
    }

    /**
     * Callback when a task is executed.
     *
     * @param task the task that executed
     * @param now the current time
     * @param forceCheck post-process regardless of running state?
     */
    protected void onTaskExecuted(ICalTask task, long now, boolean forceCheck) {
        if (running || forceCheck) {
            // check if the task needs to execute again today
            try {
                long endOfDay = DateHelper.getTimeInCurrentDay(now, timeZone, 23, 59, 59, 999).getTimeInMillis();
                logger.debug("Task is done executing; checking for any more runs between {} and {}", now, endOfDay);
                scheduleNextRun(task, now, false);
            } catch (Exception e) {
                logger.error("Unable to determine if task needs to run again today", e);
            }
        }
    }

    synchronized protected void clearAllTasks() {
        logger.debug("Clearing all tasks");
        executor.cancelAll();
        taskMap.clear();
    }

    synchronized protected void loadICSStream(InputStream is, long now) throws Exception {
        calendar = new CalendarBuilder().build(is);
        refreshLocalCalendarData(now, false);
    }

    synchronized protected void refreshLocalCalendarData(long now, boolean wasDayReset) throws Exception {
        if (executor == null) {
            throw new Exception("Can't load a schedule without a configured executor");
        }

        // clear all existing scheduled tasks
        clearAllTasks();

        // determine first & last milliseconds of today

        // iterate through all events and schedule them if they are supposed to run today
        ComponentList eventList = calendar.getComponents(Component.VEVENT);
        for (Object anEventList : eventList) {
            VEvent event = (VEvent) anEventList;
            ICalTask task = new ICalTask(actionManager, PROVIDER, event, this);
            task.setLatitude(latitude);
            task.setLongitude(longitude);
            if (addTask(task, now, wasDayReset)) {
                task.run(now);
            }
        }
    }

    protected void stopFileWatcher() {
        if (watcherThread != null) {
            watcherThread.interrupt();
            watcherThread = null;
        }
    }

    protected void startFileWatcher() {
        // start a new file watcher thread
        try {
            watcherThread = new FileWatcherThread(scheduleFile, this);
            watcherThread.start();
        } catch (IOException e) {
            logger.error("Error watching schedule file; changes will not be automatically detected", e);
        }
    }

    protected void restartFileWatcher() {
        stopFileWatcher();
        startFileWatcher();
    }

    protected void writeFile() {
        try {
            CalendarOutputter outputter = new CalendarOutputter();
            outputter.output(calendar, new FileOutputStream(scheduleFile));
        } catch (Exception e) {
            logger.error("Error writing schedule file", e);
        }
    }
}