Package com.almende.eve.agent.google

Source Code of com.almende.eve.agent.google.GoogleCalendarAgent

/**
* @file GoogleCalendarAgent.java
*
* @brief
* The GoogleCalendarAgent can connect to a single Google Calendar, and
* get, create, update, and delete events. The agent uses Goolges RESTful API v3
* to access a Google Calendar, and does not use any specific Java libraries
* for that. See:
* https://developers.google.com/google-apps/calendar/v3/reference/
*
* To setup authorization for a calendar agent, the method setAuthorization
* must be executed with valid authorization tokens. The agent will store
* the tokens and refresh them automatically when needed.
* To retrieve valid access tokens from google, the servlet GoogleAuth.java
* can be used. This servlet is typically running at /auth/google.
* Authorization needs to be setup only once for an agent.
*
* The GoogleCalendarAgent contains the following core methods:
*     - getEvents    Get all events in a given time window
*     - getEvent     Get a specific event by its id
*     - createEvent  Create a new event
*     - updateEvent  Update an existing event
*     - deleteEvent  Delete an existing event
*     - getBusy      Get the busy intervals in given time window
*     - clear        Delete all stored information
*
* @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-07-03
*/


/**
*
* DOCUMENTATION:
*   https://developers.google.com/google-apps/calendar/v3/reference/
*   https://developers.google.com/google-apps/calendar/v3/reference/events#resource
*/

package com.almende.eve.agent.google;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Interval;

import com.almende.eve.agent.Agent;
import com.almende.eve.agent.CalendarAgent;
import com.almende.eve.agent.annotation.Name;
import com.almende.eve.agent.annotation.Required;
import com.almende.eve.config.Config;
import com.almende.eve.entity.calendar.Authorization;
import com.almende.eve.rpc.jsonrpc.JSONRPCException;
import com.almende.eve.rpc.jsonrpc.JSONRPCException.CODE;
import com.almende.eve.rpc.jsonrpc.jackson.JOM;
import com.almende.eve.state.State;
import com.almende.util.HttpUtil;
import com.almende.util.IntervalsUtil;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;


public class GoogleCalendarAgent extends Agent implements CalendarAgent {
  // private Logger logger = Logger.getLogger(this.getClass().getName());

  // note: config parameters google.client_id and google.client_secret
  //       are loaded from the eve configuration
  private String OAUTH_URI = "https://accounts.google.com/o/oauth2";
  private String CALENDAR_URI = "https://www.googleapis.com/calendar/v3/calendars/";
 
  /**
   * Set access token and refresh token, used to authorize the calendar agent.
   * These tokens must be retrieved via Oauth 2.0 authorization.
   * @param access_token
   * @param token_type
   * @param expires_in
   * @param refresh_token
   * @throws IOException
   */
  public void setAuthorization (
      @Name("access_token") String access_token,
      @Name("token_type") String token_type,
      @Name("expires_in") Integer expires_in,
      @Name("refresh_token") String refresh_token) throws IOException {
    State state = getState();
   
    // retrieve user information
    String url = "https://www.googleapis.com/oauth2/v1/userinfo";
    Map<String, String> headers = new HashMap<String, String>();
    headers.put("Authorization", token_type + " " + access_token);
    String resp = HttpUtil.get(url, headers);
   
    ObjectNode info = JOM.getInstance().readValue(resp, ObjectNode.class);
    String email = info.has("email") ? info.get("email").asText() : null;
    String name = info.has("name") ? info.get("name").asText() : null;
   
    DateTime expires_at = calculateExpiresAt(expires_in);
    Authorization auth = new Authorization(access_token, token_type,
        expires_at, refresh_token);
   
    // store the tokens in the state
    state.put("auth", auth);
    state.put("email", email);
    state.put("name", name);
  }
 
  /**
   * Calculate the expiration time from a life time
   * @param expires_in      Expiration time in seconds
   * @return
   */
  private DateTime calculateExpiresAt(Integer expires_in) {
    DateTime expires_at = null;
    if (expires_in != null && expires_in != 0) {
      // calculate expiration time, and subtract 5 minutes for safety
      expires_at = DateTime.now().plusSeconds(expires_in).minusMinutes(5);
    }
    return expires_at;
  }
   
  /**
   * Refresh the access token using the refresh token
   * the tokens in provided authorization object will be updated
   * @param auth
   * @throws Exception
   */
  private void refreshAuthorization (Authorization auth) throws Exception {
    String refresh_token = (auth != null) ? auth.getRefreshToken() : null;
    if (refresh_token == null) {
      throw new Exception("No refresh token available");
    }
   
    Config config = getAgentFactory().getConfig();
    String client_id = config.get("google", "client_id");
    String client_secret = config.get("google", "client_secret");
   
    // retrieve new access_token using the refresh_token
    Map<String, String> params = new HashMap<String, String>();
    params.put("client_id", client_id);
    params.put("client_secret", client_secret);
    params.put("refresh_token", refresh_token);
    params.put("grant_type", "refresh_token");
    String resp = HttpUtil.postForm(OAUTH_URI + "/token", params);
    ObjectNode json = JOM.getInstance().readValue(resp, ObjectNode.class);
    if (!json.has("access_token")) {
      // TODO: give more specific error message
      throw new Exception("Retrieving new access token failed");
    }
   
    // update authorization
    if (json.has("access_token")) {
      auth.setAccessToken(json.get("access_token").asText());
    }
    if (json.has("expires_in")) {
      Integer expires_in = json.get("expires_in").asInt();
      DateTime expires_at = calculateExpiresAt(expires_in);
      auth.setExpiresAt(expires_at);
    }
  }
 
  /**
   * Remove all stored data from this agent
   */
  @Override
  public void delete() {
    State state = getState();
    state.remove("auth");
    state.remove("email");
    state.remove("name");

    super.delete();
  }
 
  /**
   * Get the username associated with the calendar
   * @return name
   */
  @Override
  public String getUsername() {
    return (String) getState().get("name");
  }
 
  /**
   * Get the email associated with the calendar
   * @return email
   */
  @Override
  public String getEmail() {
    return (String) getState().get("email");
  }
 
  /**
   * Get ready-made HTTP request headers containing the authorization token
   * Example usage: HttpUtil.get(url, getAuthorizationHeaders());
   * @return
   * @throws Exception
   */
  private Map<String, String> getAuthorizationHeaders () throws Exception {
    Authorization auth = getAuthorization();
   
    String access_token = (auth != null) ? auth.getAccessToken() : null;
    if (access_token == null) {
      throw new Exception("No authorization token available");
    }
    String token_type = (auth != null) ? auth.getTokenType() : null;
    if (token_type == null) {
      throw new Exception("No token type available");
    }
   
    Map<String, String> headers = new HashMap<String, String>();
    headers.put("Authorization", token_type + " " + access_token);
    return headers;
  }
 
  /**
   * Retrieve authorization tokens
   * @return
   * @throws Exception
   */
  private Authorization getAuthorization() throws Exception {
    Authorization auth = (Authorization) getState().get("auth");

    // check if access_token is expired
    DateTime expires_at = (auth != null) ? auth.getExpiresAt() : null;
    if (expires_at != null && expires_at.isBeforeNow()) {
      refreshAuthorization(auth);
      getState().put("auth", auth);
    }
   
    return auth;
  }
 
  /**
   * Get the calendar agents version
   */
  @Override
  public String getVersion() {
    return "0.4";
  }
 
  /**
   * Get the calendar agents description
   */
  @Override
  public String getDescription() {
    return "This agent gives access to a Google Calendar. " +
        "It allows to search events, find free timeslots, " +
        "and add, edit, or remove events.";
  }

  /**
   * Convert the event from a Eve event to a Google event
   * @param event
   */
  private void toGoogleEvent(ObjectNode event) {
    if (event.has("agent") && event.get("agent").isTextual()) {
      // move agent url from event.agent to extendedProperties
      String agent = event.get("agent").asText();
      event.with("extendedProperties").with("shared").put("agent", agent);
     
      // TODO: change location into a string
    }
  }

  /**
   * Convert the event from a Google event to a Eve event
   * @param event
   */
  private void toEveEvent(ObjectNode event) {
    ObjectNode extendedProperties = (ObjectNode) event.get("extendedProperties");
    if (extendedProperties != null) {
      ObjectNode shared = (ObjectNode) extendedProperties.get("shared");
      if (shared != null && shared.has("agent") && shared.get("agent").isTextual()) {
        // move agent url from extended properties to event.agent
        String agent = shared.get("agent").asText();
        event.put("agent", agent);
       
        /* TODO: remove agent from extended properties
        shared.remove("agent");
        if (shared.size() == 0) {
          extendedProperties.remove("shared");
          if (extendedProperties.size() == 0) {
            event.remove("extendedProperties");
          }
        }
        */
       
        // TODO: replace string location with Location object
      }
    }
  }
 
  /**
   * Retrieve a list with all calendars in this google calendar
   */
  @Override
  public ArrayNode getCalendarList() throws Exception {
    String url = CALENDAR_URI + "users/me/calendarList";
    String resp = HttpUtil.get(url, getAuthorizationHeaders());
    ObjectNode calendars = JOM.getInstance().readValue(resp, ObjectNode.class);

    // check for errors
    if (calendars.has("error")) {
      ObjectNode error = (ObjectNode)calendars.get("error");
      throw new JSONRPCException(error);
    }

    // get items from response
    ArrayNode items = null;
    if (calendars.has("items")) {
      items = (ArrayNode)calendars.get("items");
    }
    else {
      items = JOM.createArrayNode();
    }
   
    return items;
  }

  /**
   * Get todays events. A convenience method for easy testing
   * @param calendarId
   * @return
   * @throws Exception
   */
  public ArrayNode getEventsToday(
      @Required(false) @Name("calendarId") String calendarId) throws Exception {
    DateTime now = DateTime.now();
    DateTime timeMin = now.minusMillis(now.getMillisOfDay());
    DateTime timeMax = timeMin.plusDays(1);

    return getEvents(timeMin.toString(), timeMax.toString(), calendarId);
  }
 
  /**
   * Get all events in given interval
   * @param timeMin     start of the interval
   * @param timeMax     end of the interval
   * @param calendarId     optional calendar id. If not provided, the default
   *                      calendar is used
   */
  @Override
  public ArrayNode getEvents(
      @Required(false) @Name("timeMin") String timeMin,
      @Required(false) @Name("timeMax") String timeMax,
      @Required(false) @Name("calendarId") String calendarId)
      throws Exception {
    // initialize optional parameters
    if (calendarId == null) {
      calendarId = (String) getState().get("email");
    }
   
    // built url with query parameters
    String url = CALENDAR_URI + calendarId + "/events";
    Map<String, String> params = new HashMap<String, String>();
    if (timeMin != null) {
      params.put("timeMin", new DateTime(timeMin).toString());
    }
    if (timeMax != null) {
      params.put("timeMax", new DateTime(timeMax).toString());
    }
    // Set singleEvents=true to expand recurring events into instances
    params.put("singleEvents", "true");
    url = HttpUtil.appendQueryParams(url, params);
   
    // perform GET request
    Map<String, String> headers = getAuthorizationHeaders();
    String resp = HttpUtil.get(url, headers);
    ObjectMapper mapper = JOM.getInstance();
    ObjectNode json = mapper.readValue(resp, ObjectNode.class);
   
    // check for errors
    if (json.has("error")) {
      ObjectNode error = (ObjectNode)json.get("error");
      throw new JSONRPCException(error);
    }

    // get items from the response
    ArrayNode items = null;
    if (json.has("items")){
      items = (ArrayNode) json.get("items");
     
      // convert from Google to Eve event
      for (int i = 0; i < items.size(); i++) {
        ObjectNode item = (ObjectNode) items.get(i);
        toEveEvent(item);
      }
    }
    else {
      items = JOM.createArrayNode();
    }
   
    return items;
  }

  /**
   * Get busy intervals of today. A convenience method for easy testing
   * @param calendarId
   * @return
   * @throws Exception
   */
  public ArrayNode getBusyToday(
      @Required(false) @Name("calendarId") String calendarId,
      @Required(false) @Name("excludeEventIds") Set<String> excludeEventIds)
      throws Exception {
    DateTime now = DateTime.now();
    DateTime timeMin = now.minusMillis(now.getMillisOfDay());
    DateTime timeMax = timeMin.plusDays(1);
    String dateTimeZone = null;

    return getBusy(timeMin.toString(), timeMax.toString(), calendarId,
        excludeEventIds, dateTimeZone);
  }
 
  /**
   * Get the start time from a google event (including all-day-events)
   * Returns null if not found
   * @param event
   * @param timeZone   Timezone, needed for all-day-events
   * @return start
   */
  private static DateTime getStart(ObjectNode event, DateTimeZone timeZone) {
    if (!event.has("start")) {
      return null;
    }
    JsonNode startObj = event.get("start");
   
    DateTime start = null;
    if (startObj.has("dateTime") && !startObj.get("dateTime").isNull()) {
      String dateTimeStr = startObj.get("dateTime").asText();
      start = new DateTime(dateTimeStr);
    }
    else if (startObj.has("date") && !startObj.get("date").isNull()) {
      String dateStr = startObj.get("date").asText();
     
      if (startObj.has("timeZone") && !startObj.get("timeZone").isNull()) {
        String timeZoneStr = startObj.get("timeZone").asText();
        timeZone = DateTimeZone.forID(timeZoneStr);
      }
      if (timeZone != null) {
        start = new DateTime(dateStr, timeZone);
      }
      else {
        start = new DateTime(dateStr);
      }
    }
    else {
      start = null;
    }
   
    return start;
  }
 
  /**
   * Get the end time from a google event (including all-day-events)
   * Returns null if not found
   * @param event
   * @param timeZone   Timezone, needed for all-day-events
   * @return end
   */
  private static DateTime getEnd(ObjectNode event, DateTimeZone timeZone) {
    if (!event.has("end")) {
      return null;
    }
    JsonNode endObj = event.get("end");
   
    DateTime end = null;
    if (endObj.has("dateTime") && !endObj.get("dateTime").isNull()) {
      String dateTimeStr = endObj.get("dateTime").asText();
      end = new DateTime(dateTimeStr);
    }
    else if (endObj.has("date") && !endObj.get("date").isNull()) {
      String dateStr = endObj.get("date").asText();
     
      if (endObj.has("timeZone") && !endObj.get("timeZone").isNull()) {
        String timeZoneStr = endObj.get("timeZone").asText();
        timeZone = DateTimeZone.forID(timeZoneStr);
      }
      if (timeZone != null) {
        end = new DateTime(dateStr, timeZone);
      }
      else {
        end = new DateTime(dateStr);
      }
    }
    else {
      end = null;
    }
   
    return end;
  }

  /**
   * Retrieve the busy intervals in the calendar
   * @param timeMin         Start time
   * @param timeMax         End time
   * @param calendarId      Optional calendar id. the primary calendar is
   *                         used by default
   * @param excludeEventIds Optional list with ids of events to be excluded
   *                         from the busy intervals.
   * @param timeZone        Optional time zone. UTC is used by default.
   *                         Needed to correctly process all-day-events.
   * @throws Exception
   */
  @Override
  public ArrayNode getBusy(
      @Name("timeMin") String timeMin,
      @Name("timeMax") String timeMax,
      @Required(false) @Name("calendarId") String calendarId,
      @Required(false) @Name("excludeEventIds") Set<String> excludeEventIds,
      @Required(false) @Name("timeZone") String timeZone )
      throws Exception {
    DateTimeZone dtz = DateTimeZone.UTC;
    if (timeZone != null) {
      dtz = DateTimeZone.forID(timeZone);
    }   
   
    ArrayNode events = getEvents(timeMin, timeMax, calendarId);
   
    List<Interval> busy = new ArrayList<Interval>();
        for (int i = 0; i < events.size(); i++) {
          ObjectNode event = (ObjectNode) events.get(i);
         
          // filter excludes
          String eventId = event.has("id") ? event.get("id").asText() : null;
          boolean exclude = (eventId != null && excludeEventIds != null &&
              excludeEventIds.contains(eventId));
          if (!exclude) {
            DateTime start = getStart(event, dtz);
            DateTime end = getEnd(event, dtz);
            if (start != null && end != null) {
              Interval interval = new Interval(start, end);
              busy.add(interval);
            }
          }
        }

        // merge the intervals
        List<Interval> merged = IntervalsUtil.merge(busy);
       
        // convert to JSON array
        ArrayNode array = JOM.createArrayNode();
        for (Interval interval : merged) {
          ObjectNode obj = JOM.createObjectNode();
          obj.put("start", interval.getStart().toString());
          obj.put("end", interval.getEnd().toString());
          array.add(obj);
        }
        return array;
  }

  /**
   * Get a single event by id
   * @param eventId         Id of the event
   * @param calendarId      Optional calendar id. the primary calendar is
   *                         used by default
   */
  @Override
  public ObjectNode getEvent (
      @Name("eventId") String eventId,
      @Required(false) @Name("calendarId") String calendarId)
      throws Exception {
    // initialize optional parameters
    if (calendarId == null) {
      calendarId = (String) getState().get("email");
    }

    // built url
    String url = CALENDAR_URI + calendarId + "/events/" + eventId;
   
    // perform GET request
    Map<String, String> headers = getAuthorizationHeaders();
    String resp = HttpUtil.get(url, headers);
    ObjectMapper mapper = JOM.getInstance();
    ObjectNode event = mapper.readValue(resp, ObjectNode.class);
   
    // convert from Google to Eve event
    toEveEvent(event);
   
    // check for errors
    if (event.has("error")) {
      ObjectNode error = (ObjectNode)event.get("error");
      Integer code = error.has("code") ? error.get("code").asInt() : null;
      if (code != null && (code.equals(404) || code.equals(410))) {
        throw new JSONRPCException(CODE.NOT_FOUND);       
      }
     
      throw new JSONRPCException(error);
    }
   
    // check if canceled. If so, return null
    // TODO: be able to retrieve canceled events?
    if (event.has("status") && event.get("status").asText().equals("cancelled")) {
      throw new JSONRPCException(CODE.NOT_FOUND);
    }
   
    return event;
  }

  /**
   * Create an event
   * @param event           JSON structure containing the calendar event
   * @param calendarId      Optional calendar id. the primary calendar is
   *                         used by default
   * @return createdEvent   JSON structure with the created event
   */
  @Override
  public ObjectNode createEvent (@Name("event") ObjectNode event,
      @Required(false) @Name("calendarId") String calendarId)
      throws Exception {
    // initialize optional parameters
    if (calendarId == null) {
      calendarId = (String) getState().get("email");
    }

    // built url
    String url = CALENDAR_URI + calendarId + "/events";

    // convert from Google to Eve event
    toGoogleEvent(event);
   
    // perform POST request
    ObjectMapper mapper = JOM.getInstance();
    String body = mapper.writeValueAsString(event);
    Map<String, String> headers = getAuthorizationHeaders();
    headers.put("Content-Type", "application/json");
    String resp = HttpUtil.post(url, body, headers);
    ObjectNode createdEvent = mapper.readValue(resp, ObjectNode.class);
   
    // convert from Google to Eve event
    toEveEvent(event);
   
    // check for errors
    if (createdEvent.has("error")) {
      ObjectNode error = (ObjectNode)createdEvent.get("error");
      throw new JSONRPCException(error);
    }
   
    return createdEvent;
  }

  /**
   * Quick create an event
   * @param start
   * @param end
   * @param summary
   * @param location
   * @param calendarId
   * @return
   * @throws Exception
   */
  public ObjectNode createEventQuick (
      @Required(false) @Name("start") String start,
      @Required(false) @Name("end") String end,
      @Required(false) @Name("summary") String summary,
      @Required(false) @Name("location") String location,
      @Required(false) @Name("calendarId") String calendarId) throws Exception {
    ObjectNode event = JOM.createObjectNode();
   
    if (start == null) {
      // set start to current time, rounded to hours
      DateTime startDate = DateTime.now();
      startDate = startDate.plusHours(1);
      startDate = startDate.minusMinutes(startDate.getMinuteOfHour());
      startDate = startDate.minusSeconds(startDate.getSecondOfMinute());
      startDate = startDate.minusMillis(startDate.getMillisOfSecond());
      start = startDate.toString();
    }
    ObjectNode startObj = JOM.createObjectNode();
    startObj.put("dateTime", start);
    event.put("start", startObj);
    if (end == null) {
      // set end to start +1 hour
      DateTime startDate = new DateTime(start);
      DateTime endDate = startDate.plusHours(1);
      end = endDate.toString();
    }
    ObjectNode endObj = JOM.createObjectNode();
    endObj.put("dateTime", end);
    event.put("end", endObj);
    if (summary != null) {
      event.put("summary", summary);
    }
    if (location != null) {
      event.put("location", location);
    }
   
    return createEvent(event, calendarId);
  }
 
  /**
   * Update an existing event
   * @param event           JSON structure containing the calendar event
   *                         (event must have an id)
   * @param calendarId      Optional calendar id. the primary calendar is
   *                         used by default
   * @return updatedEvent   JSON structure with the updated event
   */
  @Override
  public ObjectNode updateEvent (@Name("event") ObjectNode event,
      @Required(false) @Name("calendarId") String calendarId)
      throws Exception {
    // initialize optional parameters
    if (calendarId == null) {
      calendarId = (String) getState().get("email");
    }

    // convert from Eve to Google event
    toGoogleEvent(event);

    // read id from event
    String id = event.get("id").asText();
    if (id == null) {
      throw new Exception("Parameter 'id' missing in event");
    }
   
    // built url
    String url = CALENDAR_URI + calendarId + "/events/" + id;
   
    // perform POST request
    ObjectMapper mapper = JOM.getInstance();
    String body = mapper.writeValueAsString(event);
    Map<String, String> headers = getAuthorizationHeaders();
    headers.put("Content-Type", "application/json");
    String resp = HttpUtil.put(url, body, headers);
    ObjectNode updatedEvent = mapper.readValue(resp, ObjectNode.class);
   
    // check for errors
    if (updatedEvent.has("error")) {
      ObjectNode error = (ObjectNode)updatedEvent.get("error");
      throw new JSONRPCException(error);
    }

    // convert from Google to Eve event
    toEveEvent(event);
   
    return updatedEvent;
  }
 
  /**
   * Delete an existing event
   * @param eventId         id of the event to be deleted
   * @param calendarId      Optional calendar id. the primary calendar is
   *                         used by default
   */
  @Override
  public void deleteEvent (@Name("eventId") String eventId,
      @Required(false) @Name("calendarId") String calendarId)
      throws Exception {
    // initialize optional parameters
    if (calendarId == null) {
      calendarId = (String) getState().get("email");
    }

    // built url
    String url = CALENDAR_URI + calendarId + "/events/" + eventId;
   
    // perform POST request
    Map<String, String> headers = getAuthorizationHeaders();
    String resp = HttpUtil.delete(url, headers);
    if (!resp.isEmpty()) {
      ObjectNode node = JOM.getInstance().readValue(resp, ObjectNode.class);
     
      // check error code
      if (node.has("error")) {
        ObjectNode error = (ObjectNode) node.get("error");
        Integer code = error.has("code") ? error.get("code").asInt() : null;
        if (code != null && (code.equals(404) || code.equals(410))) {
          throw new JSONRPCException(CODE.NOT_FOUND);       
        }
       
        throw new JSONRPCException(error);
      }
      else {
        throw new Exception(resp);
      }
    }
  }
}

TOP

Related Classes of com.almende.eve.agent.google.GoogleCalendarAgent

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.