package core; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Timer; import org.apache.log4j.Logger; import polly.reminds.MSG; import polly.reminds.MyPlugin; import de.skuzzle.polly.sdk.AbstractDisposable; import de.skuzzle.polly.sdk.FormatManager; import de.skuzzle.polly.sdk.IrcManager; import de.skuzzle.polly.sdk.MailManager; import de.skuzzle.polly.sdk.MyPolly; import de.skuzzle.polly.sdk.PersistenceManagerV2; import de.skuzzle.polly.sdk.PersistenceManagerV2.Atomic; import de.skuzzle.polly.sdk.PersistenceManagerV2.Write; import de.skuzzle.polly.sdk.Types.BooleanType; import de.skuzzle.polly.sdk.Types.StringType; import de.skuzzle.polly.sdk.Types.TimespanType; import de.skuzzle.polly.sdk.User; import de.skuzzle.polly.sdk.UserManager; import de.skuzzle.polly.sdk.eventlistener.IrcUser; import de.skuzzle.polly.sdk.exceptions.CommandException; import de.skuzzle.polly.sdk.exceptions.DatabaseException; import de.skuzzle.polly.sdk.exceptions.DisposingException; import de.skuzzle.polly.sdk.exceptions.EMailException; import de.skuzzle.polly.sdk.roles.RoleManager; import de.skuzzle.polly.sdk.time.Milliseconds; import de.skuzzle.polly.sdk.time.Time; import entities.RemindEntity; public class RemindManagerImpl extends AbstractDisposable implements RemindManager { private final static RemindFormatter MAIL_FORMAT = new MailRemindFormatter(); private final static String SUBJECT = MSG.remindMngrSubject; private final static long AUTO_SNOOZE_WAIT_TIME = Milliseconds.fromMinutes(5); public final static RemindFormatter DEFAULT_FORMAT = PatternRemindFormatter.forPattern(MyPlugin.REMIND_FORMAT_VALUE.getValue()); // XXX: special case for clum private final static RemindFormatter heidiFormat = new HeidiRemindFormatter(); private final Map<User, RemindEntity> lastReminds; private final MailManager mails; private final IrcManager irc; private final PersistenceManagerV2 persistence; private final UserManager userManager; private final RoleManager roleManager; private final Timer remindScheduler; private final Map<Integer, RemindTask> scheduledReminds; private final Map<String, RemindFormatter> specialFormats; private final Map<String, RemindEntity> sleeping; private final ActionCounter actionCounter; private final ActionCounter messageCounter; private final FormatManager formatter; private final RemindDBWrapper dbWrapper; private final Logger logger; public RemindManagerImpl(MyPolly myPolly) { this.mails = myPolly.mails(); this.irc = myPolly.irc(); this.persistence = myPolly.persistence(); this.userManager = myPolly.users(); this.formatter = myPolly.formatting(); this.roleManager = myPolly.roles(); this.lastReminds = new HashMap<User, RemindEntity>(); this.dbWrapper = new RemindDBWrapperImpl(myPolly.persistence()); this.remindScheduler = new Timer("REMIND_SCHEDULER", true); //$NON-NLS-1$ this.scheduledReminds = new HashMap<Integer, RemindManager.RemindTask>(); this.specialFormats = new HashMap<String, RemindFormatter>(); this.sleeping = new HashMap<String, RemindEntity>(); this.actionCounter = new ActionCounter(); this.messageCounter = new ActionCounter(); this.logger = Logger.getLogger(myPolly.getLoggerName(this.getClass())); // XXX: special case for clum: this.specialFormats.put("clum", heidiFormat); //$NON-NLS-1$ } @Override public RemindDBWrapper getDatabaseWrapper() { return this.dbWrapper; } @Override public synchronized RemindEntity getLastRemind(User user) { return this.lastReminds.get(user); } @Override public synchronized void addRemind(User executor, final RemindEntity remind, boolean schedule) throws DatabaseException { this.logger.info("Adding " + remind + ", schedule: " + schedule); //$NON-NLS-1$ //$NON-NLS-2$ this.dbWrapper.addRemind(remind); this.lastReminds.put(executor, remind); if (schedule) { this.scheduleRemind(remind); } if (remind.isOnAction()) { this.logger.trace("Storing remind as on return action."); //$NON-NLS-1$ this.actionCounter.put(remind.getForUser()); } if (remind.isMessage()) { this.logger.trace("Storing remind as leave message."); //$NON-NLS-1$ this.messageCounter.put(remind.getForUser()); } } @Override public void deleteRemind(int id) throws DatabaseException { RemindEntity remind = this.dbWrapper.getRemind(id); this.logger.trace("Deleting remind with id " + id); //$NON-NLS-1$ this.deleteRemind(remind); } @Override public synchronized void deleteRemind(RemindEntity remind) throws DatabaseException { if (remind != null) { this.cancelScheduledRemind(remind.getId()); this.dbWrapper.deleteRemind(remind); Iterator<RemindEntity> it = this.lastReminds.values().iterator(); while(it.hasNext()) { if (it.next() == remind) { it.remove(); } } } else { this.logger.warn("tried to delete non-existent remind."); //$NON-NLS-1$ } } @Override public void deleteRemind(User executor, int id) throws CommandException, DatabaseException { this.logger.debug("User '" + executor + " wants to delete remind with id " + id); //$NON-NLS-1$ //$NON-NLS-2$ RemindEntity remind = this.dbWrapper.getRemind(id); checkRemind(executor, remind, id); this.deleteRemind(remind); } @Override public void deleteRemind(User executor) throws DatabaseException { final RemindEntity re = this.lastReminds.get(executor); if (re == null) { throw new DatabaseException(MSG.remindMngrNoRemind); } this.deleteRemind(re); } @Override public synchronized void deliverRemind(RemindEntity remind, boolean checkIdleStatus) throws DatabaseException, EMailException { User forUser = getUser(remind.getForUser()); this.logger.info("Trying to deliver " + remind + " for " + forUser); //$NON-NLS-1$ //$NON-NLS-2$ try { if (remind.isMail()) { deliverNowMail(remind, forUser, false); } else { this.logger.trace("Remind is to be delivered in IRC. Checking user state"); //$NON-NLS-1$ boolean idle = isIdle(forUser) && checkIdleStatus; boolean online = this.irc.isOnline(forUser.getCurrentNickName()); this.logger.trace("Idle state: " + idle + ", online state: " + online); //$NON-NLS-1$ //$NON-NLS-2$ if (!online || idle) { deliverLater(remind, forUser, idle, online); } else { deliverNowIrc(remind, forUser, online); } } } finally { this.logger.trace("Now deleting " + remind); //$NON-NLS-1$ this.deleteRemind(remind); } } @Override public void deliverLater(final RemindEntity remind, User forUser, boolean wasIdle, boolean online) throws DatabaseException, EMailException { this.logger.trace("Delivering later. Checking if remind schould be delivered as mail"); //$NON-NLS-1$ boolean asMail = ((BooleanType) forUser.getAttribute( MyPlugin.LEAVE_AS_MAIL)).getValue(); boolean doubleDelivery = ((BooleanType) forUser.getAttribute( MyPlugin.REMIND_DOUBLE_DELIVERY)).getValue(); this.logger.trace("As Mail: " + asMail); //$NON-NLS-1$ this.logger.trace("Double-delivery: " + doubleDelivery); //$NON-NLS-1$ if (asMail && wasIdle) { // user was idle and wanted email notification deliverNowIrc(remind, forUser, online); deliverNowMail(remind, forUser, wasIdle); } else if (asMail) { // user was offline and wanted email deliverNowMail(remind, forUser, wasIdle); } else if (wasIdle) { // user was online and wanted no email notification: notify now and when he // returns deliverNowIrc(remind, forUser, online); if (doubleDelivery) { RemindEntity onAction = new RemindEntity(remind.getMessage(), remind.getFromUser(), remind.getForUser(), remind.getOnChannel(), remind.getDueDate(), true, remind.getLeaveDate()); onAction.setIsMessage(true); addRemind(forUser, onAction, false); } } else { RemindEntity message = new RemindEntity(remind.getMessage(), remind.getFromUser(), remind.getForUser(), remind.getOnChannel(), remind.getDueDate(), remind.getLeaveDate()); message.setWasRemind(true); message.setIsMessage(true); addRemind(forUser, message, false); } } @Override public void deliverNowIrc(RemindEntity remind, User forUser, boolean online) { if (!online) { return; } this.logger.trace("Delivering " + remind + " now in IRC"); //$NON-NLS-1$ //$NON-NLS-2$ RemindFormatter formatter = getFormat(forUser); String message = formatter.format(remind, this.formatter); boolean inChannel = this.irc.isOnChannel( remind.getOnChannel(), remind.getForUser()); // If the user is not on the specified channel, the remind is delivered in query String channel = inChannel ? remind.getOnChannel() : remind.getForUser(); // onAction messages are always delivered as qry if (remind.isOnAction()) { this.logger.trace("Remind was onAction. Removing it from onActionSet"); //$NON-NLS-1$ channel = remind.getForUser(); this.actionCounter.take(remind.getForUser()); } // decrease counter of undelivered reminds for that user if (remind.isMessage()) { this.messageCounter.take(remind.getForUser()); } this.irc.sendMessage(channel, message, this); boolean qry = channel.equals(remind.getForUser()); if (qry && (!remind.getForUser().equals(remind.getFromUser()))) { // send notice to user who left this remind if it was delivered in qry this.irc.sendMessage(remind.getForUser(), MSG.bind(MSG.remindMngrDelivered, remind.getForUser(), remind.getMessage()), this); } putToSleep(remind, forUser); checkTriggerAutoSnooze(forUser); } private final void checkTriggerAutoSnooze(User forUser) { final BooleanType autoSnooze = (BooleanType) forUser.getAttribute( MyPlugin.AUTO_SNOOZE); if (!this.userManager.isSignedOn(forUser)) { return; } else if (!autoSnooze.getValue()) { return; } final String indicator = ((StringType) forUser.getAttribute( MyPlugin.AUTO_SNOOZE_INDICATOR)).getValue(); this.irc.sendMessage(forUser.getCurrentNickName(), MSG.bind(MSG.remindMngrAutoSnoozeActive, indicator), this); new AutoSnoozeRunLater("AUTO_SNOOZE_WAITER", forUser, //$NON-NLS-1$ AUTO_SNOOZE_WAIT_TIME, this.irc, this, this.formatter).start(); } @Override public void deliverNowMail(RemindEntity remind, User forUser, boolean wasIdle) throws DatabaseException, EMailException { this.logger.trace("Delivering " + remind + " now as mail"); //$NON-NLS-1$ //$NON-NLS-2$ String mail = ((StringType) forUser.getAttribute(MyPlugin.EMAIL)).getValue(); if (mail.equals(MyPlugin.DEFAULT_EMAIL)) { this.logger.warn("Destination user has no valid email address set"); //$NON-NLS-1$ RemindEntity r = new RemindEntity( MSG.bind(MSG.remindMngrMailFail, remind.getForUser()), this.irc.getNickname(), remind.getFromUser(), remind.getFromUser(), Time.currentTime(), Time.currentTime()); // schedule this Remind for now so it will be delivered immediately. // if user is not online, it will automatically be delivered later // by the policy implemented in #deliverLater addRemind(forUser, r, true); } else { String subject = String.format(SUBJECT, remind.getMessage(), this.formatter.formatDate(remind.getDueDate())); String message = MAIL_FORMAT.format(remind, this.formatter); // if user was online, wait for reaction before sending remind as mail if (wasIdle) { new MailRunLater(forUser, this.irc, this.mails, subject, message, mail).start(); } else { this.mails.sendMail(mail, subject, message); } } } @Override public void scheduleRemind(RemindEntity remind) { this.scheduleRemind(remind, remind.getDueDate()); } @Override public void scheduleRemind(RemindEntity remind, Date dueDate) { this.logger.trace("Scheduling remind " + remind + ". Due date: " + dueDate); //$NON-NLS-1$ //$NON-NLS-2$ RemindTask task = new RemindTask(this, remind); synchronized (this.scheduledReminds) { this.scheduledReminds.put(remind.getId(), task); } this.remindScheduler.schedule(task, dueDate); } @Override public void cancelScheduledRemind(RemindEntity remind) { this.cancelScheduledRemind(remind.getId()); } @Override public void cancelScheduledRemind(int id) { this.logger.trace("Cancelling scheduled remind with id " + id); //$NON-NLS-1$ RemindTask task = null; synchronized (this.scheduledReminds) { task = this.scheduledReminds.get(id); if (task != null) { task.cancel(); this.scheduledReminds.remove(id); } } } @Override public void putToSleep(RemindEntity remind, User forUser) { this.logger.trace("Remembering " + remind + " for snooze"); //$NON-NLS-1$ //$NON-NLS-2$ synchronized (this.sleeping) { this.sleeping.put(remind.getForUser(), remind); } // get sleep time: final TimespanType sleepTime = (TimespanType) forUser.getAttribute(MyPlugin.SNOOZE_TIME); this.logger.trace("Snooze time for " + forUser + ": " + sleepTime); //$NON-NLS-1$ //$NON-NLS-2$ if (sleepTime.getSpan() > 0) { SleepTask task = new SleepTask(this, remind.getForUser()); this.remindScheduler.schedule(task, sleepTime.getSpan() * 1000); } } @Override public RemindEntity cancelSleep(RemindEntity remind) { return this.cancelSleep(remind.getForUser()); } @Override public RemindEntity cancelSleep(String forUser) { this.logger.trace("Cancelling snooze for user " + forUser); //$NON-NLS-1$ synchronized (this.sleeping) { return this.sleeping.remove(forUser); } } @Override public RemindEntity snooze(User executor, Date dueDate) throws CommandException, DatabaseException { this.logger.trace("User " + executor + " requested snooze"); //$NON-NLS-1$ //$NON-NLS-2$ RemindEntity existing; synchronized (this.sleeping) { existing = this.sleeping.get(executor.getCurrentNickName()); this.cancelSleep(executor.getCurrentNickName()); } if (existing == null) { throw new CommandException(MSG.remindMngrNoSnooze); } // if no explicit date is given, schedule new remind as long as the old was // running if (dueDate == null) { this.logger.trace("No duedate given. Calculating runtime of remind to snooze."); //$NON-NLS-1$ long runtime = existing.getDueDate().getTime() - existing.getLeaveDate().getTime(); dueDate = new Date(Time.currentTimeMillis() + runtime); this.logger.trace("Remind runtime is: " + this.formatter.formatTimeSpanMs(runtime)); //$NON-NLS-1$ } RemindEntity newRemind = existing.copyForNewDueDate(dueDate); addRemind(executor, newRemind, true); return newRemind; } @Override public RemindEntity snooze(User executor) throws DatabaseException, CommandException { boolean useSnoozeTime = ((BooleanType) executor.getAttribute( MyPlugin.USE_SNOOZE_TIME)).getValue(); if (useSnoozeTime) { TimespanType defaultRemindTime = (TimespanType) executor.getAttribute(MyPlugin.DEFAULT_REMIND_TIME); return this.snooze(executor, new Date(Time.currentTimeMillis() + defaultRemindTime.getSpan() * 1000)); } else { return this.snooze(executor, null); } } @Override public RemindEntity getSnoozabledRemind(String name) { synchronized (this.sleeping) { return this.sleeping.get(name); } } @Override public RemindEntity toggleIsMail(User executor, int id) throws DatabaseException, CommandException { final RemindEntity remind = this.persistence.atomic().find( RemindEntity.class, id); this.logger.trace("Toggeling delivery of " + remind); //$NON-NLS-1$ checkRemind(executor, remind, id); this.persistence.writeAtomic(new Atomic() { @Override public void perform(Write write) { remind.setIsMail(!remind.isMail()); } }); this.logger.trace("New delivery type: " + (remind.isMail() ? "Mail" : "IRC")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ return remind; } @Override public User getUser(String nickName) { this.logger.trace("Getting user for name '" + nickName + "'"); //$NON-NLS-1$ //$NON-NLS-2$ User u = this.userManager.getUser(nickName); if (u == null) { this.logger.trace("User is unknown, creating new one"); //$NON-NLS-1$ u = this.userManager.createUser(nickName, ""); //$NON-NLS-1$ } if (u.getCurrentNickName() == null) { u.setCurrentNickName(nickName); } return u; } @Override public RemindFormatter getFormat(User user) { RemindFormatter special = this.specialFormats.get( user.getCurrentNickName().toLowerCase()); if (special != null) { return special; } final String pattern = ((StringType) user.getAttribute(MyPlugin.REMIND_FORMAT_NAME)).getValue(); if (pattern == null) { return DEFAULT_FORMAT; } return PatternRemindFormatter.forPattern(pattern, true); } @Override public boolean isIdle(User user) { final TimespanType remindIdleTime = (TimespanType) user.getAttribute(MyPlugin.REMIND_IDLE_TIME); return Time.currentTimeMillis() - user.getLastMessageTime() > Milliseconds.fromSeconds(remindIdleTime.getSpan()); } @Override public boolean isOnActionAvailable(String forUser) { return this.actionCounter.available(forUser); } @Override public boolean isStale(String forUser) { return this.messageCounter.available(forUser); } @Override public RemindEntity modifyRemind(User executor, int id, final Date dueDate, final String msg) throws CommandException, DatabaseException { this.logger.trace("User " + executor + " requested to modify remind with id " + id); //$NON-NLS-1$ //$NON-NLS-2$ final RemindEntity remind = this.dbWrapper.getRemind(id); checkRemind(executor, remind, id); this.cancelScheduledRemind(id); this.persistence.writeAtomic(new Atomic() { @Override public void perform(Write write) { if (dueDate != null) { remind.setDueDate(dueDate); } if (msg != null) { remind.setMessage(msg); } } }); this.scheduleRemind(remind, dueDate == null ? remind.getDueDate() : dueDate); return remind; } @Override public boolean canEdit(User user, RemindEntity remind) { return remind.getForUser().equals(user.getCurrentNickName()) || remind.getFromUser().equals(user.getCurrentNickName()) || this.roleManager.hasPermission(user, MyPlugin.MODIFY_OTHER_REMIND_PERMISSION); } @Override public void checkRemind(User user, RemindEntity remind, int id) throws CommandException { this.logger.trace("Checking for sufficient rights of user " + user + " for " + remind); //$NON-NLS-1$ //$NON-NLS-2$ if (remind == null) { throw new CommandException(MSG.bind(MSG.remindMngrNoRemindWithId, id)); } else if (!canEdit(user, remind)) { throw new CommandException(MSG.bind(MSG.remindMngrNoPermission, id)); } } @Override public void traceNickChange(IrcUser oldUser, final IrcUser newUser) { this.logger.trace("tracing nickchange " + oldUser + " -> " + newUser); //$NON-NLS-1$ //$NON-NLS-2$ User oldForUser = getUser(oldUser.getNickName()); final BooleanType track = (BooleanType) oldForUser.getAttribute( MyPlugin.REMIND_TRACK_NICKCHANGE); if (!track.getValue()) { this.logger.trace("User doesnt want this nickchange to be tracked"); //$NON-NLS-1$ return; } final List<RemindEntity> reminds = this.dbWrapper.getRemindsForUser( oldUser.getNickName()); User newForUser = getUser(newUser.getNickName()); this.actionCounter.moveUser(oldUser.getNickName(), newUser.getNickName()); this.messageCounter.moveUser(oldUser.getNickName(), newUser.getNickName()); try { // HACK: this resets the sleep time RemindEntity sleeping = this.cancelSleep(oldUser.getNickName()); if (sleeping != null) { putToSleep(sleeping, newForUser); } if (reminds.isEmpty()) { return; } this.persistence.writeAtomic(new Atomic() { @Override public void perform(Write write) { for (RemindEntity remind : reminds) { remind.setForUser(newUser.getNickName()); } } }); } catch (DatabaseException e) { } } @Override public void rescheduleAll() { this.logger.trace("Scheduling all existing reminds for their duedate"); //$NON-NLS-1$ List<RemindEntity> allReminds = this.dbWrapper.getAllReminds(); synchronized (this.scheduledReminds) { for (RemindEntity r : allReminds) { this.logger.trace("Scheduling remind " + r + ". Due date: " + r.getDueDate()); //$NON-NLS-1$ //$NON-NLS-2$ if (r.getDueDate().getTime() < 0) { this.logger.warn("Skipping " + r + " because of negative due date"); //$NON-NLS-1$ //$NON-NLS-2$ continue; } try { RemindTask task = new RemindTask(this, r); this.scheduledReminds.put(r.getId(), task); this.remindScheduler.schedule(task, r.getDueDate()); if (r.isMessage()) { this.messageCounter.put(r.getForUser()); } if (r.isOnAction()) { this.actionCounter.put(r.getForUser()); } } catch (Exception e) { this.logger.error("Error while scheduling " + r, e); //$NON-NLS-1$ } } } } @Override protected void actualDispose() throws DisposingException { this.remindScheduler.cancel(); this.sleeping.clear(); this.actionCounter.clear(); this.scheduledReminds.clear(); this.specialFormats.clear(); } }