Package com.almende.eve.agent

Source Code of com.almende.eve.agent.MeetingAgent

/**
* @brief
* The MeetingAgent can dynamically schedule a meeting with multiple attendees.
*
* The MeetingAgent synchronizes a meeting for one or multiple attendees,
* and dynamically schedules the meeting on a free time slot in all calendars,
* reckoning with office hours (Mon-Fri, 9:00-17:00, CET). The duration,
* summary, location, and one or multiple attendees can be specified.
* After having created a meeting, the meeting can be updated (add/remove
* attendees, change summary, duration, start time, etc.). The meetings can be
* changed in both your Google Calendar and via the MeetingAgent itself.
*
* The MeetingAgent regularly checks for updates its meeting and reschedules it
* when needed. The update frequency depends on the time the meeting was last
* changed. When just changed, the MeetingAgent checks every 10 seconds, and
* this interval is linearly decreased towards once an hour.
*
* The MeetingAgent uses Activity as data structure, and uses this structure
* to describe a meeting. To setup a MeetingAgent call the method setActivity
* or updateActivity. The MeetingAgent will automatically start scheduling and
* monitoring the meeting, and stops with monitoring once the event is past.
*
* Core methods are:
*     setActivity     Clear current meeting and setup a new meeting
*     updateActivity  Update current meeting
*     update          Force an update: synchronize and reschedule the meeting
*     clear           Remove the meetings from the attendees calendars, and
*                     delete all stored information.
*
* A minimal, valid Activity structure looks like:
*     {
*         "summary": "Test C",
*         "constraints": {
*             "attendees": [
*                 {
*                     "agent": "http://myserver.com/agents/googlecalendaragent/123/",
*                 },
*                 {
*                     "agent": "http://myserver.com/agents/googlecalendaragent/456/",
*                 }
*             ]
*         }
*     }
*
*
* @license
* 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.
*
* Copyright © 2012 Almende B.V.
*
* @author   Jos de Jong, <jos@almende.org>
* @date   2012-08-09
*/

package com.almende.eve.agent;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Logger;

import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.joda.time.Interval;
import org.joda.time.MutableDateTime;

import com.almende.eve.agent.annotation.Access;
import com.almende.eve.agent.annotation.AccessType;
import com.almende.eve.agent.annotation.Name;
import com.almende.eve.agent.annotation.Required;
import com.almende.eve.entity.Issue;
import com.almende.eve.entity.Issue.TYPE;
import com.almende.eve.entity.Weight;
import com.almende.eve.entity.activity.Activity;
import com.almende.eve.entity.activity.Attendee;
import com.almende.eve.entity.activity.Attendee.RESPONSE_STATUS;
import com.almende.eve.entity.activity.Preference;
import com.almende.eve.entity.activity.Status;
import com.almende.eve.entity.calendar.AgentData;
import com.almende.eve.rpc.jsonrpc.JSONRPCException;
import com.almende.eve.rpc.jsonrpc.JSONRequest;
import com.almende.eve.rpc.jsonrpc.jackson.JOM;
import com.almende.eve.state.State;
import com.almende.util.IntervalsUtil;
import com.almende.util.WeightsUtil;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

public class MeetingAgent extends Agent {
  private Logger logger = Logger.getLogger(this.getClass().getName());
  private final int LOOK_AHEAD_DAYS = 7; // number of days to look ahead when
                         // planning a meeting
  private final Double WEIGHT_BUSY_OPTIONAL_ATTENDEE = -1.0;
  private final Double WEIGHT_OFFICE_HOURS = 10.0;
  private final Double WEIGHT_PREFERRED_INTERVAL = 0.1;
  // private final Double WEIGHT_UNDESIRED_INTERVAL = -0.1;
  private final Double WEIGHT_DELAY_PER_DAY = -0.1;

  /**
   * Convenience method to quickly set a new activity.
   * Currently stored activity will be removed.
   *
   * @param summary
   *            Description for the meeting
   * @param location
   * @param duration
   *            Duration in minutes
   * @param agents
   *            List with calendar agent urls of the attendees
   */
  public void setActivityQuick(@Name("summary") String summary,
      @Required(false) @Name("location") String location,
      @Name("duration") Integer duration,
      @Name("agents") List<String> agents) {
    Activity activity = new Activity();
    activity.setSummary(summary);
    activity.withConstraints().withLocation().setSummary(location);
    for (String agent : agents) {
      Attendee attendee = new Attendee();
      attendee.setAgent(agent);
      activity.withConstraints().withAttendees().add(attendee);
    }

    update();
  }

  /**
   * Set a new activity. Currently stored activity will be removed.
   * @param activity
   * @return
   * @throws Exception
   */
  public Activity setActivity(@Name("activity") Activity activity)
      throws Exception {
    clear();

    return updateActivity(activity);
  }

  /**
   * Get a set with attendee agent urls from an activity returns an empty list
   * when no attendees
   *
   * @param activity
   * @return
   */
  private Set<String> getAgents(Activity activity) {
    Set<String> agents = new TreeSet<String>();
    for (Attendee attendee : activity.withConstraints().withAttendees()) {
      String agent = attendee.getAgent();
      if (agent != null) {
        agents.add(agent);
      }
    }

    return agents;
  }

  /**
   * update the activity for meeting agent
   *
   * @param activity
   * @throws Exception
   */
  public Activity updateActivity(@Name("activity") Activity updatedActivity)
      throws Exception {
    Activity activity = (Activity) getState().get("activity");
    if (activity == null) {
      activity = new Activity();
    }

    Set<String> prevAttendees = getAgents(activity);

    // if no updated timestamp is provided, set the timestamp to now
    if (updatedActivity.withStatus().getUpdated() == null) {
      updatedActivity.withStatus().setUpdated(DateTime.now().toString());
    }

    // synchronize with the stored activity
    activity = Activity.sync(activity, updatedActivity);

    // ensure the url of the meeting agent is filled in
    String myUrl = getFirstUrl();
    activity.setAgent(myUrl);

    // create duration when missing
    Long duration = activity.withConstraints().withTime().getDuration();
    if (duration == null) {
      duration = Duration.standardHours(1).getMillis(); // 1 hour in ms
      activity.withConstraints().withTime().setDuration(duration);
    }

    // remove calendar events from removed attendees
    Set<String> currentAttendees = getAgents(activity);
    Set<String> removedAttendees = new TreeSet<String>(prevAttendees);
    removedAttendees.removeAll(currentAttendees);
    for (String attendee : removedAttendees) {
      clearAttendee(attendee);
    }

    getState().put("activity", activity);

    // update all attendees, start timer to regularly check
    update();

    return (Activity) getState().get("activity");
  }

  /**
   * Get meeting summary
   *
   * @return
   */
  public String getSummary() {
    Activity activity = (Activity) getState().get("activity");
    return (activity != null) ? activity.getSummary() : null;
  }

  /**
   * get meeting activity returns null if no activity has been initialized.
   * @return activity
   */
  public Activity getActivity() {
    return (Activity) getState().get("activity");
  }

  /**
   * The the complete state of the agent.
   * TODO: remove this temporary method
   * @return state
   */
  public Object getEverything() {
    return getState();
  }

  /**
   * Apply the constraints of the the activity (for example duration)
   *
   * @param activity
   * @return changed    Returns true if the activity is changed
   */
  private boolean applyConstraints() {
    Activity activity = (Activity) getState().get("activity");
    boolean changed = false;
    if (activity == null) {
      return false;
    }

    // constraints on attendees/resources
    /* TODO: copy actual attendees to status.attendees
    List<Attendee> constraintsAttendees = activity.withConstraints().withAttendees();
    List<Attendee> attendees = new ArrayList<Attendee>();
    for (Attendee attendee : constraintsAttendees) {
      attendees.add(attendee.clone());
    }
    activity.withStatus().setAttendees(attendees);
    // TODO: is it needed to check if the attendees are changed?
    */
   
    // check time constraints
    Long duration = activity.withConstraints().withTime().getDuration();
    if (duration != null) {
      String start = activity.withStatus().getStart();
      String end = activity.withStatus().getEnd();
      if (start != null && end != null) {
        DateTime startTime = new DateTime(start);
        DateTime endTime = new DateTime(end);
        Interval interval = new Interval(startTime, endTime);
        if (interval.toDurationMillis() != duration) {
          logger.info("status did not match constraints. "
              + "Changed end time to match the duration of "
              + duration + " ms");

          // duration does not match. adjust the end time
          endTime = startTime.plus(duration);
          activity.withStatus().setEnd(endTime.toString());
          activity.withStatus().setUpdated(DateTime.now().toString());

          changed = true;
        }
      }
    }
   
    // location constraints
    String newLocation = activity.withConstraints().withLocation().getSummary();
    String oldLocation = activity.withStatus().withLocation().getSummary();
    if (newLocation != null && !newLocation.equals(oldLocation)) {
      activity.withStatus().withLocation().setSummary(newLocation);
      changed = true;
    }
   
    if (changed) {
      // store the updated activity
      getState().put("activity", activity);
    }
    return changed;
  }

  /**
   * synchronize the meeting in all attendees calendars
   */
  private boolean syncEvents() {
    logger.info("syncEvents started");
    Activity activity = getActivity();

    boolean changed = false;
    if (activity != null) {
      String updatedBefore = activity.withStatus().getUpdated();

      for (Attendee attendee : activity.withConstraints().withAttendees()) {
        String agent = attendee.getAgent();
        if (agent != null) {
          if (attendee.getResponseStatus() != RESPONSE_STATUS.declined) {
            syncEvent(agent);
          }
          else {
            clearAttendee(agent);
          }
        }
      }

      activity = getActivity();
      String updatedAfter = activity.withStatus().getUpdated();

      changed = !updatedBefore.equals(updatedAfter);
    }
   
    return changed;
  }

  /**
   * Schedule or re-schedule the meeting. Synchronize the events, retrieve
   * busy profiles, re-schedule the event.
   */
  public void update() {
    // TODO: optimize the update method
    logger.info("update started");

    // stop running tasks
    stopAutoUpdate();
   
    clearIssues();
   
    // synchronize the events
    boolean changedEvent = syncEvents();
    if (changedEvent) {
      syncEvents();
    }

    // Check if the activity is finished
    // If not, schedule a new update task. Else we are done
    Activity activity = getActivity();
    String start =   (activity != null) ? activity.withStatus().getStart() : null;
    String updated = (activity != null) ? activity.withStatus().getUpdated() : null;
    boolean isFinished = false;
    if (start != null && (new DateTime(start)).isBefore(DateTime.now())) {
      // start of the event is in the past
      isFinished = true;
      if (updated != null && (new DateTime(updated)).isAfter(new DateTime(start))) {
        // if changed after the last planned start time, then it is
        // updated afterwards, so do not mark as finished
        isFinished = false;
      }
    }
    if (activity != null && !isFinished) {
      // not yet finished. Reschedule the activity
      updateBusyIntervals();

      boolean changedConstraints = applyConstraints();
      boolean rescheduled = scheduleActivity();
     
      if (changedConstraints || rescheduled) {
        changedEvent = syncEvents();
        if (changedEvent) {
          syncEvents();
        }
      }

      // TODO: not so nice adjusting the activityStatus here this way
      activity = getActivity();
      if (activity.withStatus().getActivityStatus() != Status.ACTIVITY_STATUS.error) {
        // store status of a activity as "planned"
        activity.withStatus().setActivityStatus(Status.ACTIVITY_STATUS.planned);
        getState().put("activity", activity);
      }

      startAutoUpdate();
    } else {
      // store status of a activity as "executed"
      activity.withStatus().setActivityStatus(Status.ACTIVITY_STATUS.executed);
      getState().put("activity", activity);

      logger.info("The activity is over, my work is done. Goodbye world.");
    }
  }

  /**
   * Get the timestamp rounded to the next half hour
   * @return
   */
  private DateTime getNextHalfHour() {
    DateTime next = DateTime.now();
    next = next.minusMillis(next.getMillisOfSecond());
    next = next.minusSeconds(next.getSecondOfMinute());

    if (next.getMinuteOfHour() > 30) {
      next = next.minusMinutes(next.getMinuteOfHour());
      next = next.plusMinutes(60);
    } else {
      next = next.minusMinutes(next.getMinuteOfHour());
      next = next.plusMinutes(30);
    }

    return next;
  }

  /**
   * Schedule the meeting based on currently known event status, infeasible
   * intervals, and preferences
   *
   * @return rescheduled Returns true if the activity has been rescheduled
   *         When rescheduled, events must be synchronized again with
   *         syncEvents.
   */
  private boolean scheduleActivity() {
    logger.info("scheduleActivity started"); // TODO: cleanup
    State state = getState();
    Activity activity = (Activity) state.get("activity");
    if (activity == null) {
      return false;
    }
   
    // read planned start and end from the activity
    DateTime activityStart = null;
    if (activity.withStatus().getStart() != null) {
      activityStart = new DateTime(activity.withStatus().getStart());
    }
    DateTime activityEnd = null;
    if (activity.withStatus().getEnd() != null) {
      activityEnd = new DateTime(activity.withStatus().getEnd());
    }
    Interval activityInterval = null;
    if (activityStart != null && activityEnd != null) {
      activityInterval = new Interval(activityStart, activityEnd);
    }

    // calculate solutions
    List<Weight> solutions = calculateSolutions();
    if (solutions.size() > 0) {
      // there are solutions. yippie!
      Weight solution = solutions.get(0);
      if (activityInterval == null ||
          !solution.getInterval().equals(activityInterval)) {
        // interval is changed, save new interval
        Status status = activity.withStatus();
        status.setStart(solution.getStart().toString());
        status.setEnd(solution.getEnd().toString());
        status.setActivityStatus(Status.ACTIVITY_STATUS.planned);
        status.setUpdated(DateTime.now().toString());
        state.put("activity", activity);
        logger.info("Activity replanned at " + solution.toString()); // TODO: cleanup logging
        try {
          // TODO: cleanup
          logger.info("Replanned activity: " + JOM.getInstance().writeValueAsString(activity));
        } catch (Exception e) {}
        return true;
      }
      else {
        // planning did not change. nothing to do.
      }
    }
    else {
      if (activityStart != null || activityEnd != null) {
        // no solution
        Issue issue = new Issue();
        issue.setCode(Issue.NO_PLANNING);
        issue.setType(Issue.TYPE.error);
        issue.setMessage("No free interval found for the meeting");
        issue.setTimestamp(DateTime.now().toString());
        // TODO: generate hints
        addIssue(issue);
 
        Status status = activity.withStatus();
        status.setStart(null);
        status.setEnd(null);
        status.setActivityStatus(Status.ACTIVITY_STATUS.error);
        status.setUpdated(DateTime.now().toString());
        state.put("activity", activity);
        logger.info(issue.getMessage()); // TODO: cleanup logging
        return true;
      }
      else {
        // planning did not change (no solution was already the case)
      }
    }
   
    return false;
  }

  /**
   * Calculate all feasible intervals with their preference weight, based on
   * the event status, stored infeasible intervals, and preferred intervals.
   * If there are no solutions, an empty array is returned.
   * @return solutions
   */
  @SuppressWarnings("unchecked")
  private List<Weight> calculateSolutions() {
    logger.info("calculateSolutions started"); // TODO: cleanup
   
    State state = getState();
    List<Weight> solutions = new ArrayList<Weight>();
   
    // get the activity
    Activity activity = (Activity) state.get("activity");
    if (activity == null) {
      return solutions;
    }
   
    // get infeasible intervals
    List<Interval> infeasible = (List<Interval>) state.get("infeasible");
    if (infeasible == null) {
      infeasible = new ArrayList<Interval>();
    }
   
    // get preferred intervals
    List<Weight> preferred = (List<Weight>) state.get("preferred");
    if (preferred == null) {
      preferred = new ArrayList<Weight>();
    }
   
    // get the duration of the activity
    Long durationLong = activity.withConstraints().withTime().getDuration();
    Duration duration = null;
    if (durationLong != null) {
      duration = new Duration(durationLong);
    } else {
      // TODO: give error when duration is not defined?
      duration = Duration.standardHours(1);
    }
   
    // check interval at next half hour
    DateTime firstTimeslot = getNextHalfHour();
    Interval test = new Interval(firstTimeslot, firstTimeslot.plus(duration));
    testInterval(infeasible, preferred, test, solutions);
   
    // loop over all infeasible intervals
    for (Interval i : infeasible) {
      // test timeslot left from the infeasible interval
      test = new Interval(i.getStart().minus(duration), i.getStart());
      testInterval(infeasible, preferred, test, solutions);
     
      // test timeslot right from the infeasible interval
      test = new Interval(i.getEnd(), i.getEnd().plus(duration));
      testInterval(infeasible, preferred, test, solutions);
    }

    // loop over all preferred intervals
    for (Weight w : preferred) {
      // test timeslot left from the start of the preferred interval
      test = new Interval(w.getStart().minus(duration), w.getStart());
      testInterval(infeasible, preferred, test, solutions);

      // test timeslot right from the start of the preferred interval
      test = new Interval(w.getStart(), w.getStart().plus(duration));
      testInterval(infeasible, preferred, test, solutions);
     
      // test timeslot left from the end of the preferred interval
      test = new Interval(w.getEnd().minus(duration), w.getEnd());
      testInterval(infeasible, preferred, test, solutions);
     
      // test timeslot right from the end of the preferred interval
      test = new Interval(w.getEnd(), w.getEnd().plus(duration));
      testInterval(infeasible, preferred, test, solutions);
    }
   
    // order the calculated feasible timeslots by weight, from highest to
    // lowest. In case of equals weights, the timeslots are ordered by
    // start date
    class WeightComparator implements Comparator<Weight> {
      @Override
      public int compare(Weight a, Weight b) {
        if (a.getWeight() != null && b.getWeight() != null) {
          int cmp = Double.compare(a.getWeight(), b.getWeight());
          if (cmp == 0) {
            return a.getStart().compareTo(b.getStart());
          }
          else {
            return -cmp;
          }
        }
        return 0;
      }
    }
    WeightComparator comparator = new WeightComparator();
    Collections.sort(solutions, comparator);

    // remove duplicates
    int i = 1;
    while (i < solutions.size()) {
      if (solutions.get(i).equals(solutions.get(i - 1))) {
        solutions.remove(i);
      }
      else {
        i++;
      }
    }
   
    return solutions;
  }
 
  /**
   * Test if given interval is feasible. If so, calculate the preference
   * weight and add it to the provided array with solutions
   * @param infeasible
   * @param preferred
   * @param test
   * @param solutions
   */
  private void testInterval(final List<Interval> infeasible,
      final List<Weight> preferred, final Interval test,
      List<Weight> solutions) {
    boolean feasible = calculateFeasible(infeasible, test);
    if (feasible) {
      double weight = calculatePreference(preferred, test);
      solutions.add(new Weight(test, weight));
    }
  }

  /**
   * Start automatic updating
   * The interval of the update task depends on the timestamp the activity
   * is last updated. When recently updated, the interval is smaller.
   * interval is  minimum 10 sec and maximum 1 hour.
   * @throws IOException
   * @throws JSONRPCException
   * @throws JsonMappingException
   * @throws JsonParseException
   */
  public void startAutoUpdate() {
    State state = getState();
    Activity activity = getActivity();
   
    // determine the interval (1 hour by default)
    long TEN_SECONDS = 10 * 1000;
    long ONE_HOUR = 60 * 60 * 1000;
    long interval = ONE_HOUR; // default is 1 hour
    if (activity != null) {
      String updated = activity.withStatus().getUpdated();
      if (updated != null) {
        DateTime dateUpdated = new DateTime(updated);
        DateTime now = DateTime.now();
        interval = new Interval(dateUpdated, now).toDurationMillis();
      }
    }
    if (interval < TEN_SECONDS) {
      interval = TEN_SECONDS;
    }
    if (interval > ONE_HOUR) {
      interval = ONE_HOUR;
    }
   
    // stop any running task
    stopAutoUpdate();

    // schedule an update task and store the task id
    JSONRequest request = new JSONRequest("update", null);
    String task = getScheduler().createTask(request, interval);
    state.put("updateTask", task);

    logger.info("Auto update started. Interval = " + interval
        + " milliseconds");
  }

  /**
   * Stop automatic updating
   */
  public void stopAutoUpdate() {
    State state = getState();

    String task = (String) state.get("updateTask");
    if (task != null) {
      getScheduler().cancelTask(task);
      state.remove("updateTask");
    }

    logger.info("Auto update stopped");
  }

  /**
   * Convert a calendar event into an activity
   * @param event
   * @return activity
   */
  private Activity convertEventToActivity(ObjectNode event) {
    Activity activity = new Activity();
   
    // agent
    String agent = null;
    if (event.has("agent")) {
      agent = event.get("agent").asText();
    }
    activity.setAgent(agent);

    // summary
    String summary = null;
    if (event.has("summary")) {
      summary = event.get("summary").asText();
    }
    activity.setSummary(summary);

    // description
    String description = null;
    if (event.has("description")) {
      description = event.get("description").asText();
    }
    activity.setDescription(description);
   
    // updated
    String updated = null;
    if (event.has("updated")) {
      updated = event.get("updated").asText();
    }
    activity.withStatus().setUpdated(updated);

    // start
    String start = null;
    if (event.with("start").has("dateTime")) {
      start = event.with("start").get("dateTime").asText();
    }
    activity.withStatus().setStart(start);

    // end
    String end = null;
    if (event.with("end").has("dateTime")) {
      end = event.with("end").get("dateTime").asText();
    }
    activity.withStatus().setEnd(end);

    // duration
    if (start != null && end != null) {
      Interval interval = new Interval(new DateTime(start), new DateTime(
          end));
      Long duration = interval.toDurationMillis();
      activity.withConstraints().withTime().setDuration(duration);
    }

    // location
    String location = null;
    if (event.has("location")) {
      location = event.get("location").asText();
    }
    activity.withConstraints().withLocation().setSummary(location);
   
    return activity;
  }

  /**
   * Merge an activity into an event
   * All fields that are in the event will be left as they are
   * @param event
   * @param activity
   */
  private void mergeActivityIntoEvent(ObjectNode event, Activity activity) {
    // merge static information
    event.put("agent", activity.getAgent());
    event.put("summary", activity.getSummary());
    event.put("description", activity.getDescription());
   
    /// merge status information
    Status status = activity.withStatus();
    event.put("updated", status.getUpdated());
    event.with("start").put("dateTime", status.getStart());
    event.with("end").put("dateTime", status.getEnd());
    event.put("location", status.withLocation().getSummary());
  }

  /**
   * Retrieve all current issues. If there are no issues, an empty array
   * is returned
   * @return
   */
  @SuppressWarnings("unchecked")
  public ArrayList<Issue> getIssues() {
    ArrayList<Issue> issues = (ArrayList<Issue>) getState().get("issues");
    if (issues == null) {
      issues = new ArrayList<Issue>();
    }
    return issues;
  }

  /**
   * Remove all issues
   */
  private void clearIssues() {
    getState().remove("issues");
  }
 
  /**
   * Add an issue to the issue list
   * The issue will trigger an event
   * @param issue
   */
  private void addIssue(Issue issue) {
    ArrayList<Issue> issues = getIssues();
    issues.add(issue);
    getState().put("issues", issues);
   
    // trigger an error event
    try {
      String event = issue.getType().toString();
      ObjectNode data = JOM.createObjectNode();
      data.put("issue", JOM.getInstance().convertValue(issue,
          ObjectNode.class));
      ObjectNode params = JOM.createObjectNode();
      params.put("description", issue.getMessage());
      params.put("data", data);
      trigger(event, params);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
 
  /**
   * Create an issue with type, code, and message
   * timestamp will be set to NOW
   * @param type
   * @param code
   * @param message
   */
  private void addIssue (TYPE type, Integer code, String message) {
    Issue issue = new Issue();
    issue.setType(type);
    issue.setCode(code);
    issue.setMessage(message);
    issue.setTimestamp(DateTime.now().toString());
    addIssue(issue);
  }
 
  /**
   * Retrieve the data of a single calendar agent from the state
   *
   * @param agentUrl
   * @return data returns the calendar data. If not available, a new, empty
   *         CalendarAgentData is returned.
   */
  // TODO: create some separate AgentData handling class, instead of methods in MeetingAgent
  @SuppressWarnings("unchecked")
  private AgentData getAgentData(String agentUrl) {
    Map<String, AgentData> calendarAgents =
        (Map<String, AgentData>) getState().get("calendarAgents");

    if (calendarAgents != null && calendarAgents.containsKey(agentUrl)) {
      return calendarAgents.get(agentUrl);
    }
    return new AgentData();
  }

  /**
   * Put data for a calendar agent into the state
   *
   * @param agentUrl
   * @param data
   */
  @SuppressWarnings("unchecked")
  private void putAgentData(String agentUrl, AgentData data) {
    State state = getState();
    HashMap<String, AgentData> calendarAgents =
        (HashMap<String, AgentData>) state.get("calendarAgents");
    if (calendarAgents == null) {
      calendarAgents = new HashMap<String, AgentData>();
    }

    calendarAgents.put(agentUrl, data);
    state.put("calendarAgents", calendarAgents);
  }

  /**
   * Remove a calendar agent data from the state
   * @param agent
   * @param data
   */
  @SuppressWarnings("unchecked")
  private void removeAgentData(String agent) {
    State state = getState();
    HashMap<String, AgentData> calendarAgents = (HashMap<String, AgentData>) state
        .get("calendarAgents");
    if (calendarAgents != null && calendarAgents.containsKey(agent)) {
      calendarAgents.remove(agent);
      state.put("calendarAgents", calendarAgents);
    }
  }

  /**
   * Retrieve the busy intervals of a calendar agent from the state
   * @param agent
   * @return busy returns busy intervals, or null if not available
   */
  private List<Interval> getAgentBusy(String agent) {
    AgentData data = getAgentData(agent);
    return data.busy;
  }

  /**
   * Put the busy intervals for a calendar agent into the state
   * @param agent
   * @param busy
   */
  private void putAgentBusy(String agent, List<Interval> busy) {
    AgentData data = getAgentData(agent);
    data.busy = busy;
    putAgentData(agent, data);
  }

  /**
   * Retrieve calendar event from calendaragent
   * @param agent
   * @return event   Calendar event, or null if not found
   */
  private ObjectNode getEvent (String agent) {
    ObjectNode event = null;
    String eventId = getAgentData(agent).eventId;
    if (eventId != null) {
      ObjectNode params = JOM.createObjectNode();
      params.put("eventId", eventId);
      try {
        event = send(agent, "getEvent", params, ObjectNode.class);
      } catch (JSONRPCException e) {
        if (e.getCode() == 404) {
          // event was deleted by the user.

          //e.printStackTrace();
          Activity activity = (Activity) getState().get("activity");
          Attendee attendee = activity.withConstraints().withAttendee(agent);
          attendee.setResponseStatus(RESPONSE_STATUS.declined);
          getState().put("activity", activity);
         
          clearAttendee(agent); // TODO: seems not to work
        } else {
          e.printStackTrace(); // TODO: remove print stacktrace
        }
      } catch (Exception e) {
        addIssue(TYPE.warning, Issue.EXCEPTION, e.getMessage());
        e.printStackTrace(); // TODO: remove print stacktrace
      }
    }
    return event;
  }

  // TODO: comment
  private boolean equalsDateTime(String a, String b) {
    if (a != null && b != null) {
      return new DateTime(a).equalsnew DateTime(b));
    }
    if (a == null && b == null) {
      return false;
    }
   
    return true;
  }
 
  /**
   * Synchronize the event with given calendar agent
   *
   * @param agent
   */
  // TODO: the method syncEvent has grown to large. split it up
  private void syncEvent(@Name("agent") String agent) {
    logger.info("syncEvent started for agent " + agent);
    State state = getState();

    // retrieve event from calendar agent
    ObjectNode event = getEvent(agent);
    if (event == null) {
      event = JOM.createObjectNode();
    }
    Activity eventActivity = convertEventToActivity(event);
   
    // verify all kind of stuff
    Activity activity = (Activity) state.get("activity");
    if (activity == null) {
      return; // oops no activity at all
    }
    if (activity.withStatus().getStart() == null ||
        activity.withStatus().getEnd() == null) {
      return; // activity is not yet planned. cancel synchronization
    }
    Attendee attendee = activity.withConstraints().getAttendee(agent);
    if (attendee == null) {
      return; // unknown attendee
    }
    if (attendee.getResponseStatus() == Attendee.RESPONSE_STATUS.declined) {
      // attendee does not want to attend
      clearAttendee(agent);
      return;
    }
   
    // check if the activity or the retrieved event is changed since the
    // last synchronization
    AgentData agentData = getAgentData(agent);
    boolean activityChanged = !equalsDateTime(agentData.activityUpdated,
        activity.withStatus().getUpdated());
    boolean eventChanged = !equalsDateTime(agentData.eventUpdated,
        eventActivity.withStatus().getUpdated());
    boolean changed = activityChanged || eventChanged;   
    if (changed && activity.isNewerThan(eventActivity)) {
      // activity is updated (event is out-dated or not yet existing)

      // merge the activity into the event
      mergeActivityIntoEvent(event, activity);
     
      // TODO: if attendee cannot attend (=optional or declined), show this somehow in the event
     
      // save the event
      ObjectNode params = JOM.createObjectNode();
      params.put("event", event);
      try {
        // TODO: only update/create the event when the attendee
        // is not optional or is available at the planned time
        String method = event.has("id") ? "updateEvent" : "createEvent";
        ObjectNode updatedEvent = send(agent, method, params,
            ObjectNode.class);

        // update the agent data
        agentData.eventId = updatedEvent.get("id").asText();
        agentData.eventUpdated = updatedEvent.get("updated").asText();
        agentData.activityUpdated = activity.withStatus().getUpdated();
        putAgentData(agent, agentData);
      } catch (JSONRPCException e) {
        addIssue(TYPE.warning, Issue.JSONRPCEXCEPTION, e.getMessage());
        e.printStackTrace(); // TODO remove printing stacktrace
      } catch (Exception e) {
        addIssue(TYPE.warning, Issue.EXCEPTION, e.getMessage());
        e.printStackTrace(); // TODO remove printing stacktrace
      }
    } else if (changed) {
      // event is updated (activity is out-dated or both have the same
      // updated timestamp)
     
      // if start is changed, add this as preferences to the constraints
      if (!equalsDateTime(activity.withStatus().getStart(),
          eventActivity.withStatus().getStart())) {
        /* TODO: store the old interval as undesired?
        String oldStart = activity.withStatus().getStart();
        String oldEnd = activity.withStatus().getEnd();
        if (oldStart != null && oldEnd != null) {
          Preference undesired = new Preference ();
          undesired.setStart(oldStart);
          undesired.setEnd(oldEnd);
          undesired.setWeight(WEIGHT_UNDESIRED_INTERVAL);
          activity.getConstraints().getTime().addPreference(undesired); 
        }
        */
       
        // store the new interval as preferred
        String newStart = eventActivity.withStatus().getStart();
        String newEnd = eventActivity.withStatus().getEnd();
        if (newStart != null && newEnd != null) {
          Preference preferred = new Preference ();
          preferred.setStart(newStart);
          preferred.setEnd(newEnd);
          preferred.setWeight(WEIGHT_PREFERRED_INTERVAL);

          // overwrite other preferences with this new preference
          // TODO: all preferences are overwritten for now. Behavior should be changed.
          List<Preference> preferences = new ArrayList<Preference>();
          preferences.add(preferred);
          activity.getConstraints().getTime().setPreferences(preferences);       
         
          //activity.getConstraints().getTime().addPreference(preferred);
        }
      }
      else {
        // events are in sync, nothing to do
      }
     
      // update the activity
      activity.merge(eventActivity);
      state.put("activity", activity);

      // update the agent data
      agentData.eventId = event.get("id").asText();
      agentData.eventUpdated = event.get("updated").asText();
      agentData.activityUpdated = activity.withStatus().getUpdated();
      putAgentData(agent, agentData);
    }
    else {
      // activity and eventActivity have the same updated timestamp
      // nothing to do.
      logger.info("event and activity are in sync"); // TODO: cleanup
    }
  }

  /**
   * Update the busy intervals of all attendees, and merge the results
   */
  private void updateBusyIntervals() {
    Activity activity = getActivity();
    if (activity != null) {
      List<Attendee> attendees = activity.withConstraints().withAttendees();
      for (Attendee attendee : attendees) {
        String agent = attendee.getAgent();
        if (attendee.getResponseStatus() != RESPONSE_STATUS.declined) {
          updateBusyInterval(agent);
        }
      }
    }
   
    mergeTimeConstraints();
  }

  /**
   * Merge the busy intervals of all attendees, and the preferred intervals
   */
  private void mergeTimeConstraints() {
    ArrayList<Interval> infeasibleIntervals = new ArrayList<Interval>();
    ArrayList<Weight> preferredIntervals = new ArrayList<Weight>();

    Activity activity = getActivity();
    if (activity != null) {
      // read and merge the stored busy intervals of all attendees
      for (Attendee attendee : activity.withConstraints().withAttendees()) {
        String agent = attendee.getAgent();
        if (attendee.getResponseStatus() == RESPONSE_STATUS.declined) {
          // This attendee declined.
          // Ignore this attendees busy interval
        }
        else if (new Boolean(true).equals(attendee.getOptional())) {
          // This attendee is optional.
          // Add its busy intervals to the soft constraints
          List<Interval> attendeeBusy = getAgentBusy(agent);
          if (attendeeBusy != null) {
            for (Interval i : attendeeBusy) {
              Weight wi = new Weight(
                  i.getStart(), i.getEnd(),
                  WEIGHT_BUSY_OPTIONAL_ATTENDEE);

              preferredIntervals.add(wi);
            }
          }         
        }
        else {
          // this attendee is required.
          // Add its busy intervals to the hard constraints
          List<Interval> attendeeBusy = getAgentBusy(agent);
          if (attendeeBusy != null) {
            infeasibleIntervals.addAll(attendeeBusy);
          }
        }       
      }

      // read the time preferences and add them to the soft constraints
      List<Preference> preferences = activity.withConstraints()
        .withTime().withPreferences();
      for (Preference p : preferences) {
        if (p != null) {
          Weight wi = new Weight(
              new DateTime(p.getStart()),
              new DateTime(p.getEnd()),
              p.getWeight());

          preferredIntervals.add(wi);
        }
      }
    }
   
    // add office hours profile to the soft constraints
    // TODO: don't include (hardcoded) office hours here, should be handled
    // by a PersonalAgent
    DateTime timeMin = DateTime.now();
    DateTime timeMax = timeMin.plusDays(LOOK_AHEAD_DAYS);
    List<Interval> officeHours = IntervalsUtil.getOfficeHours(timeMin,
        timeMax);
    for (Interval i : officeHours) {
      Weight wi = new Weight(i, WEIGHT_OFFICE_HOURS);
      preferredIntervals.add(wi);
    }
   
    // add delay penalties to the soft constraints
    DateTime now = DateTime.now();
    MutableDateTime d = new MutableDateTime(now.getYear(),
        now.getMonthOfYear(), now.getDayOfMonth(), 0, 0, 0, 0);
    for (int i = 0; i <= LOOK_AHEAD_DAYS; i++) {
      DateTime start = d.toDateTime();
      DateTime end = start.plusDays(1);
      Weight wi = new Weight(start, end,
          WEIGHT_DELAY_PER_DAY * i);
      preferredIntervals.add(wi);
      d.addDays(1);
    }

    // order and store the aggregated lists with intervals
    IntervalsUtil.order(infeasibleIntervals);
    getState().put("infeasible", infeasibleIntervals);
    WeightsUtil.order(preferredIntervals);
    getState().put("preferred", preferredIntervals);
  }
 
  /**
   * Calculate the average preference for given interval.
   * The method aggregates over all stored preferences
   * Default preference is 0.
   * @param preferredIntervals   list with intervals ordered by start
   * @param test                 test interval
   * @return preference
   */
  private double calculatePreference(
      List<Weight> preferredIntervals, Interval test) {
    double preference = 0;

    for (Weight interval : preferredIntervals) {
      Interval overlap = test.overlap(interval.getInterval());
      if (overlap != null) {
        Double weight = interval.getWeight();
        if (weight != null) {
          double durationCheck = test.toDurationMillis();
          double durationOverlap = overlap.toDurationMillis();
          double avgWeight = (durationOverlap / durationCheck) * weight;
          preference += avgWeight;
        }
      }

      if (interval.getStart().isAfter(test.getEnd())) {
        // as the list is ordered, we can exit as soon as we have an
        // interval which starts after the wanted interval.
        break;
      }
    }
   
    return preference;
  }
     
  /**
   * Calculate whether given interval is feasible (i.e. does not overlap with
   * any of the infeasible intervals, and is not in the past)
   * @param infeasibleIntervals   list with intervals ordered by start
   * @param timeMin
   * @param timeMax
   * @return feasible
   */
  private boolean calculateFeasible(List<Interval> infeasibleIntervals,
      Interval test) {
    if (test.getStart().isBeforeNow()) {
      // interval starts in the past
      return false;
    }
   
    for (Interval interval : infeasibleIntervals) {
      if (test.overlaps(interval)) {
        return false;
      }
      if (interval.getStart().isAfter(test.getEnd())) {
        // as the list is ordered, we can exit as soon as we have an
        // interval which starts after the wanted interval.
        break;
      }
    }
   
    return true;
  }
 
  /**
   * Retrieve the feasible and preferred intervals
   * @return
   */
  // TODO: remove this temporary method
  @SuppressWarnings("unchecked")
  public ObjectNode getIntervals() {
    ObjectNode intervals = JOM.createObjectNode();

    List<Interval> infeasible = (List<Interval>) getState().get("infeasible");
    List<Weight> preferred = (List<Weight>) getState().get("preferred");
    List<Weight> solutions = calculateSolutions();

    // merge the intervals
    List<Interval> mergedInfeasible = null;
    List<Weight> mergedPreferred = null;
    if (infeasible != null) {
      mergedInfeasible = IntervalsUtil.merge(infeasible);
    }
    if (preferred != null) {
      mergedPreferred = WeightsUtil.merge(preferred);
    }

    if (infeasible != null) {
      ArrayNode arr = JOM.createArrayNode();
      for (Interval interval : infeasible) {
        ObjectNode o = JOM.createObjectNode();
        o.put("start", interval.getStart().toString());
        o.put("end", interval.getEnd().toString());
        arr.add(o);
      }
      intervals.put("infeasible", arr);
    }

    if (preferred != null) {
      ArrayNode arr = JOM.createArrayNode();
      for (Weight weight : preferred) {
        ObjectNode o = JOM.createObjectNode();
        o.put("start", weight.getStart().toString());
        o.put("end", weight.getEnd().toString());
        o.put("weight", weight.getWeight());
        arr.add(o);
      }
      intervals.put("preferred", arr);
    }

    if (solutions != null) {
      ArrayNode arr = JOM.createArrayNode();
      for (Weight weight : solutions) {
        ObjectNode o = JOM.createObjectNode();
        o.put("start", weight.getStart().toString());
        o.put("end", weight.getEnd().toString());
        o.put("weight", weight.getWeight());
        arr.add(o);
      }
      intervals.put("solutions", arr);
    }

    if (mergedInfeasible != null) {
      ArrayNode arr = JOM.createArrayNode();
      for (Interval i : mergedInfeasible) {
        ObjectNode o = JOM.createObjectNode();
        o.put("start", i.getStart().toString());
        o.put("end", i.getEnd().toString());
        arr.add(o);
      }
      intervals.put("mergedInfeasible", arr);
    }

    if (mergedPreferred != null) {
      ArrayNode arr = JOM.createArrayNode();
      for (Weight wi : mergedPreferred) {
        ObjectNode o = JOM.createObjectNode();
        o.put("start", wi.getStart().toString());
        o.put("end", wi.getEnd().toString());
        o.put("weight", wi.getWeight());
        arr.add(o);
      }
      intervals.put("mergedPreferred", arr);
    }   

    return intervals;   
  }

  /**
   * Retrieve the busy intervals of a calendar agent
   *
   * @param agent
   */
  private void updateBusyInterval(@Name("agent") String agent) {
    try {
      // create parameters with the boundaries of the interval to be
      // retrieved
      ObjectNode params = JOM.createObjectNode();
      DateTime timeMin = DateTime.now();
      DateTime timeMax = timeMin.plusDays(LOOK_AHEAD_DAYS);
      params.put("timeMin", timeMin.toString());
      params.put("timeMax", timeMax.toString());

      // exclude the event managed by this agent from the busy intervals
      String eventId = getAgentData(agent).eventId;
      if (eventId != null) {
        ArrayNode excludeEventIds = JOM.createArrayNode();
        excludeEventIds.add(eventId);
        params.put("excludeEventIds", excludeEventIds);
      }

      // get the busy intervals from the agent
      ArrayNode array = send(agent, "getBusy", params, ArrayNode.class);

      // convert from ArrayNode to List
      List<Interval> busy = new ArrayList<Interval>();
      for (int i = 0; i < array.size(); i++) {
        ObjectNode obj = (ObjectNode) array.get(i);
        String start = obj.has("start") ? obj.get("start").asText()
            : null;
        String end = obj.has("end") ? obj.get("end").asText() : null;
        busy.add(new Interval(new DateTime(start), new DateTime(end)));
      }

      // store the interval in the state
      putAgentBusy(agent, busy);

    } catch (JSONRPCException e) {
      addIssue(TYPE.warning, Issue.JSONRPCEXCEPTION, e.getMessage());
      e.printStackTrace(); // TODO remove printing stacktrace
    } catch (Exception e) {
      addIssue(TYPE.warning, Issue.EXCEPTION, e.getMessage());
      e.printStackTrace(); // TODO remove printing stacktrace
    }
  }

  /**
   * Delete everything of the agent
   */
  @Override
  public void delete() {
    clear();
   
    // super class will delete the state
    super.delete();
  }

  /**
   * Clear the stored activity, and remove events from attendees.
   */
  @Access(AccessType.UNAVAILABLE)
  public void clear () {
    Activity activity = getActivity();

    if (activity != null) {
      List<Attendee> attendees = activity.withConstraints().withAttendees();
      for (Attendee attendee : attendees) {
        String agent = attendee.getAgent();
        if (agent != null) {
          clearAttendee(agent);
        }
      }
    }

    // stop auto update timer (if any)
    stopAutoUpdate();
  }
 
  /**
   * Clear an event from given agent
   *
   * @param agent
   */
  private void clearAttendee(@Name("agent") String agent) {
    AgentData data = getAgentData(agent);
    if (data != null) {
      try {
        if (data.eventId != null) {
          ObjectNode params = JOM.createObjectNode();
          params.put("eventId", data.eventId);
          send(agent, "deleteEvent", params);
          data.eventId = null;
        }
      } catch (JSONRPCException e) {
        if (e.getCode() == 404) {
          // event was already deleted. fine!
          data.eventId = null;
        }
        else {
          e.printStackTrace();
        }
      } catch (Exception e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }

      if (data.eventId == null) {
        removeAgentData(agent);
        logger.info("clearAttendee " + agent + " cleared");
      }
    }
  }

  // TODO: cleanup this temporary method
  public ArrayNode getOfficeHours(@Name("timeMin") String timeMin,
      @Name("timeMax") String timeMax) {
    List<Interval> available = IntervalsUtil.getOfficeHours(new DateTime(
        timeMin), new DateTime(timeMax));

    // convert to JSON array
    ArrayNode array = JOM.createArrayNode();
    for (Interval interval : available) {
      ObjectNode obj = JOM.createObjectNode();
      obj.put("start", interval.getStart().toString());
      obj.put("end", interval.getEnd().toString());
      array.add(obj);
    }
    return array;
  }

  /**
   * Get the first url of the agents urls. Returns null if the agent does not
   * have any urls.
   * @return firstUrl
   */
  private String getFirstUrl() {
    List<String> urls = getUrls();
    if (urls.size() > 0) {
      return urls.get(0);
    }
    return null;
  }
 
  @Override
  public String getDescription() {
    return "A MeetingAgent can dynamically plan and manage a meeting.";
  }

  @Override
  public String getVersion() {
    return "0.1";
  }
}
TOP

Related Classes of com.almende.eve.agent.MeetingAgent

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.